Merge branch 'master' of ssh://git.fedorahosted.org/git/fedora-infrastructure

This commit is contained in:
Toshio Kuratomi 2008-03-05 11:15:34 -08:00
commit 1f3e3993d4
31 changed files with 553 additions and 1005 deletions

View file

@ -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 <locale>
This will create a PO file at po/<locale>/LC_MESSAGES/fas.po
To update (merge) PO files with the POT file, run:
tg-admin i18n merge

View file

@ -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()

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <lmacken@redhat.com>
# Toshio Kuratomi <tkuratom@redhat.com>
#
'''
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('<span class="fielderror">(.*)</span>')
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

View file

@ -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"

View file

@ -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):

View file

@ -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())

View file

@ -5,17 +5,25 @@ from turbogears.database import session
from fas.auth import *
class Help(controllers.Controller):
help = { 'none' : ['Error', '<p>We could not find that help item</p>'],
'user_ircnick' : ['IRC Nick (Optional)', '<p>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</p>'],
'user_primary_email' : ['Primary Email (Required)', '<p>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</p>'],
'user_human_name' : ['Full Name (Required)', '<p>Your Human Name or "real life" name</p>'],
'user_gpg_keyid' : ['GPG Key', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. 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 <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a> for more information</p>'],
'user_telephone' : ['Telephone', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a></p>'],
'user_postal_address': ['Postal Address', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. This should be a mailing address where you can be contacted. See our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a> about any concerns.</p>'],
'user_timezone': ['Timezone (Optional)', '<p>Please specify the time zone you are in.</p>'],
'user_comments': ['Comments (Optional)', '<p>Misc comments about yourself.</p>'],
'user_account_status': ['Account Status', '<p>Shows account status, possible values include<ul><li>Valid</li><li>Disabled</li><li>Expired</li></ul></p>'],
'user_cla' : ['CLA', '<p>In order to become a full Fedora contributor you must sign a <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">Contributor License Agreement</a>. 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 <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a>.</p>'],
help = { 'none' : ['Error', '<p>We could not find that help item</p>'],
'user_ircnick' : ['IRC Nick (Optional)', '<p>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</p>'],
'user_primary_email' : ['Primary Email (Required)', '<p>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</p>'],
'user_human_name' : ['Full Name (Required)', '<p>Your Human Name or "real life" name</p>'],
'user_gpg_keyid' : ['GPG Key', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. 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 <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a> for more information</p>'],
'user_telephone' : ['Telephone', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a></p>'],
'user_postal_address': ['Postal Address', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. This should be a mailing address where you can be contacted. See our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a> about any concerns.</p>'],
'user_timezone': ['Timezone (Optional)', '<p>Please specify the time zone you are in.</p>'],
'user_comments': ['Comments (Optional)', '<p>Misc comments about yourself.</p>'],
'user_account_status': ['Account Status', '<p>Shows account status, possible values include<ul><li>Valid</li><li>Disabled</li><li>Expired</li></ul></p>'],
'user_cla' : ['CLA', '<p>In order to become a full Fedora contributor you must sign a <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">Contributor License Agreement</a>. 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 <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a>.</p>'],
'user_locale': ['Locale', '<p>For non-english speaking peoples this allows individuals to select which locale they are in.</p>'],
'group_apply': ['Apply', '<p>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 <a href="../about">about page</a>.</p>'],
'group_remove': ['Remove', '''<p>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.</p>'''],
'group_upgrade': ['Upgrade', '''<p>Upgrade a persons status in this group.<ul><li>from user -> to sponsor</li><li>From sponsor -> administrator</li><li>administrators cannot be upgraded beyond administrator</li></ul></p>'''],
'group_downgrade': ['Downgrade', '''<p>Downgrade a persons status in the group.<ul><li>from administrator -> to sponsor</li><li>From sponsor -> user</li><li>users cannot be downgraded below user, you may want to remove them</li></ul></p>'''],
'group_approve': ['Approve', '''<p>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.</p>'''],
'group_sponsor': ['Sponsor', '''<p>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.</p>'''],
}
def __init__(self):

View file

@ -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 <tkuratom@redhat.com>
#
'''
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]

View file

@ -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 <tkuratom@redhat.com>
#
'''
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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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}

View file

@ -8,10 +8,10 @@
</head>
<body>
<h2>${_('FAS - The Open Account System')}</h2>
<p>${_('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.')}</p>
<p>${_('''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.''')}</p>
<h2>${_('Etiquette')}</h2>
<p>${_('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.')}
<p>${_("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.")}</p>
<h2>${_('Users, Sponsors, Administrators')}</h2>
<p>${_('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.')}
<p>${_('''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.''')}</p>
</body>
</html>

View file

@ -9,6 +9,7 @@
<body>
<h2>${_('Fedora Contributor License Agreement')}</h2>
<p>
<!-- TODO: Update to not mention click-through CLA (until it's ready) -->
${Markup(_('There are two ways to sign the CLA. Most users will want to do a signed CLA as it will promote them to a full contributor in Fedora. The click-through CLA only grants partial access but may be preferred for those with special legal considerations. See: &lt;a href="http://fedoraproject.org/wiki/Legal/CLAAcceptanceHierarchies"&gt;CLA Acceptance Hierarchies&lt;/a&gt; for more information.'))}
</p>
<br/>

View file

@ -1,3 +1,3 @@
#for user in sorted(groups.keys())
${user},${Person.byUserName(user).mail},${Person.byUserName(user).givenName},${groups[user].fedoraRoleType}
#for role in sorted(group.approved_roles)
${role.member.username},${role.member.emails['primary'].email},${role.member.human_name},${role.role_type}
#end

View file

@ -45,6 +45,7 @@
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('Unapproved')}</span>
</a>
<a py:if="group not in person.memberships" href="${tg.url('/group/apply/%s/%s' % (group.name, person.username))}"><span>${_('Apply')}</span></a>
<script py:if="group not in person.memberships" type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/group_apply'});</script>
</td>
</tr>
</tbody>

View file

@ -27,15 +27,15 @@
</div>
<div class="field">
<label for="needs_sponsor">${_('Needs Sponsor:')}</label>
<input type="checkbox" id="needs_sponsor" name="needs_sponsor" value="TRUE" checked="checked" />
<input type="checkbox" id="needs_sponsor" name="needs_sponsor" value="1" checked="checked" />
</div>
<div class="field">
<label for="user_can_remove">${_('Self Removal:')}</label>
<input type="checkbox" id="user_can_remove" name="user_can_remove" value="TRUE" checked="checked" />
<input type="checkbox" id="user_can_remove" name="user_can_remove" value=1"" checked="checked" />
</div>
<div class="field">
<label for="prerequisite">${_('Must Belong To:')}</label>
<input type="text" id="prerequisite" name="prerequisite" value="cla_done" />
<input type="text" id="prerequisite" name="prerequisite" value="cla_sign" />
</div>
<div class="field">
<label for="joinmsg">${_('Join Message:')}</label>

View file

@ -28,6 +28,7 @@
</div>
</form>
<a py:if="group in person.memberships" href="${tg.url('/group/remove/%s/%s' % (group.name, person.username))}">${_('Remove me')}</a>
<script py:if="group in person.memberships" type="text/javascript">var hb7 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_remove')}'});</script>
<h3>Group Details <a py:if="auth.canAdminGroup(person, group)" href="${tg.url('/group/edit/%s' % group.name)}">${_('(edit)')}</a></h3>
<div class="userbox">
<dl>
@ -70,10 +71,11 @@
</thead>
<tr py:for="role in group.roles">
<td><a href="${tg.url('/user/view/%s' % role.member.username)}">${role.member.username}</a></td>
<td py:if='not(role.member.username == "None")'><a href="${tg.url('/user/view/%s' % role.member.username)}">${role.member.username}</a></td>
<td py:if='role.member.username == "None"'>${_('None')}</td>
<td py:if='role.sponsor'><a href="${tg.url('/user/view/%s' % role.sponsor.username)}">${role.sponsor.username}</a></td>
<td py:if='not role.sponsor'>${_('None')}</td>
<td>${role.creation.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}</td>
<td>${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}</td>
<td py:if='role.approval'>${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}</td>
<td py:if='not role.approval'>${_('None')}</td>
<td>${role.role_status}</td>
<td>${role.role_type}</td>
<!-- This section includes all action items -->
@ -81,16 +83,21 @@
<ul class="actions">
<li py:if="group in role.member.unapproved_memberships">
<a py:if="group.needs_sponsor" href="${tg.url('/group/sponsor/%s/%s' % (group.name, role.member.username))}">${_('Sponsor')}</a>
<script py:if="group.needs_sponsor" type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_sponsor')}'});</script>
<a py:if="not group.needs_sponsor" href="${tg.url('/group/sponsor/%s/%s' % (group.name, role.member.username))}">${_('Approve')}</a>
<script py:if="not group.needs_sponsor" type="text/javascript">var hb2 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_approve')}'});</script>
</li>
<li py:if="auth.canRemoveUser(person, group, role.member)">
<a href="${tg.url('/group/remove/%s/%s' % (group.name, role.member.username))}">${_('Remove')}</a>
<script type="text/javascript">var hb3 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_remove')}'});</script>
</li>
<li py:if="auth.canUpgradeUser(person, group, role.member)">
<a href="${tg.url('/group/upgrade/%s/%s' % (group.name, role.member.username))}">${_('Upgrade')}</a>
<script type="text/javascript">var hb4 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_upgrade')}'});</script>
</li>
<li py:if="auth.canDowngradeUser(person, group, role.member)">
<a href="${tg.url('/group/downgrade/%s/%s' % (group.name, role.member.username))}">${_('Downgrade')}</a>
<script type="text/javascript">var hb5 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_downgrade')}'});</script>
</li>
</ul>
</td>

View file

@ -7,5 +7,25 @@
<title>${_('Fedora Accounts System')}</title>
</head>
<body>
<?python from fas import auth ?>
<h2>Todo queue:</h2>
<py:for each="group in sorted(person.memberships)">
<dl>
<py:if test="auth.canSponsorGroup(person, group) and group.unapproved_roles">
<dd>
<ul class="queue">
<li py:for="role in group.unapproved_roles[:5]">
${Markup(_('&lt;strong&gt;%(user)s&lt;/strong&gt; requests approval to join &lt;a href="group/view/%(group)s"&gt;%(group)s&lt;/a&gt;.') % {'user': role.member.username, 'group': group.name, 'group': group.name})}
</li>
</ul>
</dd>
</py:if>
</dl>
</py:for>
<ul class="queue">
<li py:if="cla == None">
${_('CLA Not Signed. To become a full Fedora Contributor please ')}<a href="${tg.url('/cla/')}">${_('sign the CLA')}</a>.
</li>
</ul>
</body>
</html>

View file

@ -46,7 +46,7 @@
</div>
<div id="control">
<ul>
<li><a href="${tg.url('/about')}">about</a></li>
<li><a href="${tg.url('/about')}">About</a></li>
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/user/view/%s' % tg.identity.user.username)}">${_('My Account')}</a></li>
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/logout')}">${_('Log Out')}</a></li>
<li py:if="tg.identity.anonymous"><a href="${tg.url('/login')}">${_('Log In')}</a></li>
@ -56,13 +56,14 @@
<div id="main">
<div id="sidebar">
<ul>
<li class="first"><a href="${tg.url('/group/list')}">${_('Group List')}</a></li>
<li class="first"><a href="${tg.url('/home')}">${_('Home')}</a></li>
<div py:if="not tg.identity.anonymous and 'accounts' in tg.identity.groups" py:strip=''>
<!-- TODO: Make these use auth.py -->
<li><a href="${tg.url('/user/list')}">${_('User List')}</a></li>
<li><a href="${tg.url('/group/new')}">${_('New Group')}</a></li>
<li><a href="${tg.url('/user/list')}">${_('User List')}</a></li>
</div>
<li><a href="${tg.url('/group/list/A*')}">${_('Apply For a new Group')}</a></li>
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/group/list')}">${_('Group List')}</a></li>
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/group/list/A*')}">${_('Apply For a new Group')}</a></li>
<li><a href="http://fedoraproject.org/wiki/FWN/LatestIssue">${_('News')}</a></li>
</ul>
<div py:if="tg.identity.anonymous" id="language">
@ -74,14 +75,34 @@
</div>
</div>
<div id="content">
<div py:if="tg_flash" class="flash">
<div style="
background: #FF7777;
color: #333333;
border: 1px solid #555555;
padding: 1ex 1ex 0.5ex;
float: right;
width: 57ex;
margin: 1ex 1ex 1ex 2ex;
">
<div style="font-size: 3ex;"><strong>Warning:</strong> This is a test instance!</div>
<ul style="
font-size: 2ex;
list-style: square;
padding-left: 3ex;
">
<li>Avoid entering private info here.</li>
<li>The database may be wiped/rebuilt periodically.</li>
<li>Feel free to file bugs, enhancements, etc. at <a href="https://fedorahosted.org/fas2/">https://fedorahosted.org/fas2/</a></li>
</ul>
</div>
<div py:if="tg_flash" class="flash" style="margin-right: 62ex">
${tg_flash}
</div>
<div py:replace="select('*|text()')" />
</div>
<div id="footer">
<ul id="footlinks">
<li class="first"><a href="/">${_('About')}</a></li>
<li class="first"><a href="${tg.url('/about')}">${_('About')}</a></li>
<li><a href="http://fedoraproject.org/wiki/Communicate">${_('Contact Us')}</a></li>
<li><a href="http://fedoraproject.org/wiki/Legal">${_('Legal &amp; Privacy')}</a></li>
<!--<li><a href="/">Site Map</a></li>-->

View file

@ -12,13 +12,13 @@
<div class="field">
<label for="human_name">${_('Human Name')}:</label>
<input type="text" id="human_name" name="human_name" value="${target.human_name}" />
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/user_human_name'});</script>
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_human_name')}'});</script>
</div>
<!--Need to figure out what the interface should be for emails. -->
<div class="field">
<label for="mail">${_('Email')}:</label>
<input type="text" id="email" name="email" value="${target.emails['primary'].email}" />
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '/fas/help/get_help/user_primary_email'});</script>
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_primary_email')}'});</script>
</div>
<!-- <div class="field">
<label for="fedoraPersonBugzillaMail">${_('Bugzilla Email')}:</label>
@ -27,22 +27,22 @@
<div class="field">
<label for="ircnick">${_('IRC Nick')}:</label>
<input type="text" id="ircnick" name="ircnick" value="${target.ircnick}" />
<script type="text/javascript">var hb3 = new HelpBalloon({dataURL: '/fas/help/get_help/user_ircnick'});</script>
<script type="text/javascript">var hb3 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_ircnick')}'});</script>
</div>
<div class="field">
<label for="gpg_keyid">${_('PGP Key')}:</label>
<input type="text" id="gpg_keyid" name="gpg_keyid" value="${target.gpg_keyid}" />
<script type="text/javascript">var hb4 = new HelpBalloon({dataURL: '/fas/help/get_help/user_gpg_keyid'});</script>
<script type="text/javascript">var hb4 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_gpg_keyid')}'});</script>
</div>
<div class="field">
<label for="telephone">${_('Telephone Number')}:</label>
<input type="text" id="telephone" name="telephone" value="${target.telephone}" />
<script type="text/javascript">var hb5 = new HelpBalloon({dataURL: '/fas/help/get_help/user_telephone'});</script>
<script type="text/javascript">var hb5 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_telephone')}'});</script>
</div>
<div class="field">
<label for="postal_address">${_('Postal Address')}:</label>
<textarea id="postal_address" name="postal_address">${target.postal_address}</textarea>
<script type="text/javascript">var hb6 = new HelpBalloon({dataURL: '/fas/help/get_help/user_postal_address'});</script>
<script type="text/javascript">var hb6 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_postal_address')}'});</script>
</div>
<div class="field">
<label for="timezone">${_('Time Zone')}:</label>
@ -52,22 +52,23 @@
?>
<option py:for="tz in common_timezones" value="${tz}" py:attrs="{'selected': target.timezone == tz and 'selected' or None}">${tz}</option>
</select>
<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '/fas/help/get_help/user_timezone'});</script>
<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_timezone')}'});</script>
</div>
<div class="field">
<label for="locale">${_('Locale')}:</label>
<input type="text" id="locale" name="locale" value="${target.locale}" />
<script type="text/javascript">var hb8 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_locale')}'});</script>
<!--
<select id="locale" name="locale">
option py:for="locale in available_locales" value="${locale}" py:attrs="{'selected': target.locale == locale and 'selected' or None}">${locale}</option>
</select>
-->
<!--<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '/fas/help/get_help/locale'});</script>-->
<!--<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/locale')}'});</script>-->
</div>
<div class="field">
<label for="comments ">${_('Comments')}:</label>
<textarea id="comments" name="comments">${target.comments}</textarea>
<script type="text/javascript">var hb8 = new HelpBalloon({dataURL: '/fas/help/get_help/user_comments'});</script>
<script type="text/javascript">var hb8 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_comments')}'});</script>
</div>
<div class="field">
<input type="submit" value="${_('Save!')}" />

View file

@ -19,18 +19,21 @@
<!--<dt>${_('Bugzilla Email:')}</dt><dd>${person.username}</dd>-->
<dt>${_('IRC Nick:')}</dt><dd>${person.ircnick}&nbsp;</dd>
<dt>${_('PGP Key:')}</dt><dd>${person.gpg_keyid}&nbsp;</dd>
<dt>${_('Telephone Number:')}</dt><dd>${person.telephone}&nbsp;</dd>
<dt>${_('Postal Address:')}</dt><dd>${person.postal_address}&nbsp;</dd>
<py:if test="personal"><dt>${_('Telephone Number:')}</dt><dd>${person.telephone}&nbsp;</dd></py:if>
<py:if test="personal"><dt>${_('Postal Address:')}</dt><dd>${person.postal_address}&nbsp;</dd></py:if>
<dt>${_('Comments:')}</dt><dd>${person.comments}&nbsp;</dd>
<dt>${_('Password:')}</dt><dd><span class="approved">${_('Valid')}</span> <a href="${tg.url('/user/changepass')}" py:if="personal">(change)</a></dd>
<dt>${_('Account Status:')}</dt><dd><span class="approved">${_('Valid')}</span>
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/user_account_status'});</script></dd>
<!-- cla = {None, 'signed', 'clicked'} -->
<py:if test="personal"><dt>${_('Password:')}</dt><dd><span class="approved">${_('Valid')}</span> <a href="${tg.url('/user/changepass')}">(change)</a></dd></py:if>
<dt>${_('Account Status:')}</dt><dd>
<span py:if="person.status == 'active'" class="approved">${_('Active')}</span>
<span py:if="person.status == 'vacation'" class="approved">${_('Vacation')}</span>
<span py:if="person.status == 'inactive'" class="unapproved">${_('Inactive')}</span>
<span py:if="person.status == 'pinged'" class="approved">${_('Pinged')}</span>
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_account_status')}'});</script></dd>
<dt>${_('CLA:')}</dt><dd>
<span py:if="cla == 'signed'" class="approved">${_('Signed CLA')}</span>
<span py:if="cla == 'clicked'" class="approved">${_('Click-through CLA')}<py:if test="personal">(<a href="${tg.url('/cla/')}">${_('GPG Sign it!')}</a></py:if>)</span>
<span py:if="not cla" class="unapproved">${_('Not Done')}<py:if test="personal"> (<a href="${tg.url('/cla/')}">${_('Sign it!')}</a>)</py:if></span>
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '/fas/help/get_help/user_cla'});</script></dd>
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_cla')}'});</script></dd>
</dl>
</div>
<h3 py:if="personal">${_('Your Roles')}</h3>
@ -54,7 +57,7 @@
<dt>${_('Status:')}</dt>
<dd>
<span class="approved" py:if="group in person.approved_memberships">${_('Approved')}</span>
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('Unapproved')}</span>
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('None')}</span>
</dd>
<dt>${_('Tools:')}</dt>
<dd>

View file

@ -65,7 +65,10 @@ class ValidUsername(validators.FancyValidator):
class UserSave(validators.Schema):
targetname = KnownUser
human_name = validators.String(not_empty=True, max=42)
human_name = validators.All(
validators.String(not_empty=True, max=42),
validators.Regex(regex='^[^\n:<>]+$'),
)
#mail = validators.All(
# validators.Email(not_empty=True, strip=True, max=128),
# NonFedoraEmail(not_empty=True, strip=True, max=128),
@ -81,7 +84,10 @@ class UserCreate(validators.Schema):
validators.String(max=32, min=3),
validators.Regex(regex='^[a-z][a-z0-9]+$'),
)
human_name = validators.String(not_empty=True, max=42)
human_name = validators.All(
validators.String(not_empty=True, max=42),
validators.Regex(regex='^[^\n:<>]+$'),
)
email = validators.All(
validators.Email(not_empty=True, strip=True),
NonFedoraEmail(not_empty=True, strip=True),
@ -211,7 +217,7 @@ class User(controllers.Controller):
target = People.by_username(target)
if not canEditUser(person, target):
turbogears.flash(_("You do not have permission to edit '%s'" % target.username))
turbogears.flash(_("You do not have permission to edit '%s'") % target.username)
turbogears.redirect('/user/edit/%s', target.username)
return dict()
try:
@ -226,7 +232,7 @@ class User(controllers.Controller):
target.locale = locale
target.timezone = timezone
except TypeError:
turbogears.flash(_('Your account details could not be saved: %s' % e))
turbogears.flash(_('Your account details could not be saved: %s') % e)
else:
turbogears.flash(_('Your account details have been saved.'))
turbogears.redirect("/user/view/%s" % target.username)
@ -267,6 +273,7 @@ class User(controllers.Controller):
person.human_name = human_name
person.telephone = telephone
person.password = '*'
person.status = 'active'
person.emails['primary'] = PersonEmails(email=email, purpose='primary')
newpass = generate_password()
message = turbomail.Message(config.get('accounts_mail'), person.emails['primary'].email, _('Welcome to the Fedora Project!'))
@ -378,10 +385,16 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
# CLA one), think of how to make sure this doesn't get
# full of random keys (keep a clean Fedora keyring)
# TODO: MIME stuff?
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"))
else:
try:
plaintext = StringIO.StringIO(mail)
@ -390,7 +403,7 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
ctx.armor = True
signer = ctx.get_key(re.sub('\s', '', config.get('gpg_fingerprint')))
ctx.signers = [signer]
recipient = ctx.get_key(re.sub('\s', '', person.gpg_keyid))
recipient = ctx.get_key(keyid)
def passphrase_cb(uid_hint, passphrase_info, prev_was_bad, fd):
os.write(fd, '%s\n' % config.get('gpg_passphrase'))
ctx.passphrase_cb = passphrase_cb
@ -406,7 +419,7 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
message.plain = mail;
turbomail.enqueue(message)
try:
person.password = newpass['pass']
person.password = newpass['hash']
turbogears.flash(_('Your new password has been emailed to you.'))
except:
turbogears.flash(_('Your password could not be reset.'))
@ -419,7 +432,7 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
def gencert(self):
from fas.openssl_fas import *
username = turbogears.identity.current.user_name
person = Person.by_username(username)
person = People.by_username(username)
person.certificate_serial = person.certificate_serial + 1
@ -438,8 +451,8 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
L=config.get('openssl_l'),
O=config.get('openssl_o'),
OU=config.get('openssl_ou'),
CN=user.cn,
emailAddress=person.mail,
CN=person.username,
emailAddress=person.emails['primary'].email,
)
cert = createCertificate(req, (cacert, cakey), person.certificate_serial, (0, expire), digest='md5')

View file

@ -56,7 +56,7 @@ CREATE TABLE people (
internal_comments TEXT,
ircnick TEXT,
last_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
status TEXT,
status TEXT DEFAULT 'active',
status_change TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
locale TEXT not null DEFAULT 'C',
timezone TEXT null DEFAULT 'UTC',

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2008-03-03 00:46-0500\n"
"POT-Creation-Date: 2008-03-04 22:01-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,32 +17,36 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.1\n"
#: client/fasClient.py:42
#: client/fasClient.py:41
msgid "Download and sync most recent content"
msgstr ""
#: client/fasClient.py:47
#: client/fasClient.py:46
#, python-format
msgid "Specify config file (default \"%default\")"
msgstr ""
#: client/fasClient.py:51
msgid "Do not sync group information"
msgstr ""
#: client/fasClient.py:52
#: client/fasClient.py:56
msgid "Do not sync passwd information"
msgstr ""
#: client/fasClient.py:57
#: client/fasClient.py:61
msgid "Do not sync shadow information"
msgstr ""
#: client/fasClient.py:62
#, python-format
msgid "Specify URL of fas server (default \"%default\")"
#: client/fasClient.py:66
msgid "Specify URL of fas server."
msgstr ""
#: client/fasClient.py:67
#: client/fasClient.py:71
msgid "Enable FAS synced shell accounts"
msgstr ""
#: client/fasClient.py:72
#: client/fasClient.py:76
msgid "Disable FAS synced shell accounts"
msgstr ""
@ -59,92 +63,98 @@ msgstr ""
msgid "%s membership required before application to this group is allowed"
msgstr ""
#: fas/cla.py:52 fas/cla.py:178
#: fas/cla.py:52
msgid ""
"You have already signed the CLA, so it is unnecessary to complete the "
"Click-through CLA."
"To sign the CLA we must have your telephone number, postal address and "
"gpg key id. Please ensure they have been filled out"
msgstr ""
#: fas/cla.py:56 fas/cla.py:182
msgid "You have already completed the Click-through CLA."
msgstr ""
#: fas/cla.py:61 fas/cla.py:87
#: fas/cla.py:69 fas/cla.py:95
msgid "You have already signed the CLA."
msgstr ""
#: fas/cla.py:100 fas/user.py:380
#: fas/cla.py:108 fas/user.py:390
msgid "Your key could not be retrieved from subkeys.pgp.net"
msgstr ""
#: fas/cla.py:107
#: fas/cla.py:121
#, python-format
msgid "Your signature could not be verified: '%s'."
msgstr ""
#: fas/cla.py:117
#: fas/cla.py:131
msgid ""
"Your signature's fingerprint did not match the fingerprint registered in "
"FAS."
msgstr ""
#: fas/cla.py:126
#: fas/cla.py:140
msgid "Your key did not match your email."
msgstr ""
#: fas/cla.py:131
#: fas/cla.py:145
msgid "len(sigs) == 0"
msgstr ""
#: fas/cla.py:137
#: fas/cla.py:151
msgid "The GPG-signed part of the message did not contain a signed CLA."
msgstr ""
#: fas/cla.py:142
#: fas/cla.py:156
msgid "The text \"I agree\" was not found in the CLA."
msgstr ""
#: fas/cla.py:156 fas/cla.py:194
#: fas/cla.py:170 fas/cla.py:210
#, python-format
msgid "You could not be added to the '%s' group."
msgstr ""
#: fas/cla.py:165
#: fas/cla.py:180
#, python-format
msgid "You have successfully signed the CLA. You are now in the '%s' group."
msgstr ""
#: fas/cla.py:194
msgid ""
"You have already signed the CLA, so it is unnecessary to complete the "
"Click-through CLA."
msgstr ""
#: fas/cla.py:198
msgid "You have already completed the Click-through CLA."
msgstr ""
#: fas/cla.py:214
#, python-format
msgid ""
"You have successfully agreed to the click-through CLA. You are now in "
"the '%s' group."
msgstr ""
#: fas/cla.py:202
#: fas/cla.py:218
msgid "You have not agreed to the click-through CLA."
msgstr ""
#: fas/controllers.py:84
#: fas/controllers.py:98
#, python-format
msgid "Welcome, %s"
msgstr ""
#: fas/controllers.py:99
#: fas/controllers.py:113
msgid ""
"The credentials you supplied were not correct or did not grant access to "
"this resource."
msgstr ""
#: fas/controllers.py:102
#: fas/controllers.py:116
msgid "You must provide your credentials before accessing this resource."
msgstr ""
#: fas/controllers.py:105
#: fas/controllers.py:119
msgid "Please log in."
msgstr ""
#: fas/controllers.py:116
#: fas/controllers.py:131
msgid "You have successfully logged out."
msgstr ""
@ -158,7 +168,7 @@ msgstr ""
msgid "The group '%s' already exists."
msgstr ""
#: fas/group.py:129 fas/group.py:467
#: fas/group.py:129 fas/group.py:469
#, python-format
msgid "You cannot view '%s'"
msgstr ""
@ -207,7 +217,7 @@ msgstr ""
msgid "%(user)s has already applied to %(group)s!"
msgstr ""
#: fas/group.py:293
#: fas/group.py:295
#, python-format
msgid ""
"\n"
@ -218,22 +228,22 @@ msgid ""
"Please go to %(url)s to take action. \n"
msgstr ""
#: fas/group.py:300
#: fas/group.py:302
#, python-format
msgid "%(user)s has applied to %(group)s!"
msgstr ""
#: fas/group.py:317
#: fas/group.py:319
#, python-format
msgid "You cannot sponsor '%s'"
msgstr ""
#: fas/group.py:324
#: fas/group.py:326
#, python-format
msgid "'%s' could not be sponsored!"
msgstr ""
#: fas/group.py:329
#: fas/group.py:331
#, python-format
msgid ""
"\n"
@ -244,22 +254,22 @@ msgid ""
"%(joinmsg)s\n"
msgstr ""
#: fas/group.py:337
#: fas/group.py:339
#, python-format
msgid "'%s' has been sponsored!"
msgstr ""
#: fas/group.py:354
#: fas/group.py:356
#, python-format
msgid "You cannot remove '%s'."
msgstr ""
#: fas/group.py:361
#: fas/group.py:363
#, python-format
msgid "%(name)s could not be removed from %(group)s!"
msgstr ""
#: fas/group.py:367
#: fas/group.py:369
#, python-format
msgid ""
"\n"
@ -269,22 +279,22 @@ msgid ""
"aliases within an hour.\n"
msgstr ""
#: fas/group.py:374
#: fas/group.py:376
#, python-format
msgid "%(name)s has been removed from %(group)s!"
msgstr ""
#: fas/group.py:391
#: fas/group.py:393
#, python-format
msgid "You cannot upgrade '%s'"
msgstr ""
#: fas/group.py:401
#: fas/group.py:403
#, python-format
msgid "%(name)s could not be upgraded!"
msgstr ""
#: fas/group.py:409
#: fas/group.py:411
#, python-format
msgid ""
"\n"
@ -294,22 +304,22 @@ msgid ""
"into the e-mail aliases within an hour.\n"
msgstr ""
#: fas/group.py:416
#: fas/group.py:418
#, python-format
msgid "%s has been upgraded!"
msgstr ""
#: fas/group.py:432
#: fas/group.py:434
#, python-format
msgid "You cannot downgrade '%s'"
msgstr ""
#: fas/group.py:439
#: fas/group.py:441
#, python-format
msgid "%(username)s could not be downgraded!"
msgstr ""
#: fas/group.py:446
#: fas/group.py:448
#, python-format
msgid ""
"\n"
@ -319,16 +329,16 @@ msgid ""
"into the e-mail aliases within an hour.\n"
msgstr ""
#: fas/group.py:453
#: fas/group.py:455
#, python-format
msgid "%s has been downgraded!"
msgstr ""
#: fas/group.py:495
#: fas/group.py:497
msgid "Come join The Fedora Project!"
msgstr ""
#: fas/group.py:496
#: fas/group.py:498
#, python-format
msgid ""
"\n"
@ -349,12 +359,12 @@ msgid ""
"Fedora and FOSS are changing the world -- come be a part of it!"
msgstr ""
#: fas/group.py:513
#: fas/group.py:515
#, python-format
msgid "Message sent to: %s"
msgstr ""
#: fas/group.py:516
#: fas/group.py:518
#, python-format
msgid "You are not in the '%s' group."
msgstr ""
@ -392,39 +402,39 @@ msgstr ""
msgid "'%s' is an illegal username."
msgstr ""
#: fas/user.py:196
#: fas/user.py:205
#, python-format
msgid "You cannot edit %s"
msgstr ""
#: fas/user.py:211
#: fas/user.py:220
#, python-format
msgid "You do not have permission to edit '%s'"
msgstr ""
#: fas/user.py:226
#: fas/user.py:235
#, python-format
msgid "Your account details could not be saved: %s"
msgstr ""
#: fas/user.py:228
#: fas/user.py:237
msgid "Your account details have been saved."
msgstr ""
#: fas/user.py:242
#: fas/user.py:251
#, python-format
msgid "No users found matching '%s'"
msgstr ""
#: fas/user.py:248
#: fas/user.py:257
msgid "No need to sign up, you have an account!"
msgstr ""
#: fas/user.py:269
#: fas/user.py:278
msgid "Welcome to the Fedora Project!"
msgstr ""
#: fas/user.py:270
#: fas/user.py:279
#, python-format
msgid ""
"\n"
@ -466,42 +476,42 @@ msgid ""
"forward to working with you!\n"
msgstr ""
#: fas/user.py:310
#: fas/user.py:319
msgid ""
"Your password has been emailed to you. Please log in with it and change "
"your password"
msgstr ""
#: fas/user.py:313
#: fas/user.py:322
#, python-format
msgid "The username '%s' already Exists. Please choose a different username."
msgstr ""
#: fas/user.py:338
#: fas/user.py:348
msgid "Your password has been changed."
msgstr ""
#: fas/user.py:341
#: fas/user.py:351
msgid "Your password could not be changed."
msgstr ""
#: fas/user.py:347
#: fas/user.py:357
msgid "You are already logged in!"
msgstr ""
#: fas/user.py:356
#: fas/user.py:366
msgid "You are already logged in."
msgstr ""
#: fas/user.py:362
#: fas/user.py:372
msgid "username + email combo unknown."
msgstr ""
#: fas/user.py:365
#: fas/user.py:375
msgid "Fedora Project Password Reset"
msgstr ""
#: fas/user.py:366
#: fas/user.py:376
#, python-format
msgid ""
"\n"
@ -511,20 +521,74 @@ msgid ""
"Please go to https://admin.fedoraproject.org/fas/ to change it.\n"
msgstr ""
#: fas/user.py:399
#: fas/user.py:415
msgid ""
"Your password reset email could not be encrypted. Your password has not "
"been changed."
msgstr ""
#: fas/user.py:406
#: fas/user.py:422
msgid "Your new password has been emailed to you."
msgstr ""
#: fas/user.py:408
#: fas/user.py:424
msgid "Your password could not be reset."
msgstr ""
#: fas/templates/about.html:7
msgid "About FAS"
msgstr ""
#: fas/templates/about.html:10
msgid "FAS - The Open Account System"
msgstr ""
#: fas/templates/about.html:11
msgid ""
"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."
msgstr ""
#: fas/templates/about.html:12
msgid "Etiquette"
msgstr ""
#: fas/templates/about.html:13
msgid ""
"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."
msgstr ""
#: fas/templates/about.html:14
msgid "Users, Sponsors, Administrators"
msgstr ""
#: fas/templates/about.html:15
msgid ""
"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."
msgstr ""
#: fas/templates/error.html:7 fas/templates/home.html:7
#: fas/templates/cla/click.html:7 fas/templates/cla/index.html:7
#: fas/templates/cla/view.html:7 fas/templates/openid/about.html:7
@ -604,66 +668,66 @@ msgstr ""
msgid "Logged in:"
msgstr ""
#: fas/templates/master.html:49
#: fas/templates/master.html:50
msgid "My Account"
msgstr ""
#: fas/templates/master.html:50 fas/templates/master.html:87
#: fas/templates/master.html:51 fas/templates/master.html:88
msgid "Log Out"
msgstr ""
#: fas/templates/master.html:51 fas/templates/welcome.html:21
#: fas/templates/master.html:52 fas/templates/welcome.html:21
msgid "Log In"
msgstr ""
#: fas/templates/master.html:58
msgid "Group List"
msgstr ""
#: fas/templates/master.html:61
msgid "User List"
msgstr ""
#: fas/templates/master.html:62
msgid "New Group"
msgstr ""
#: fas/templates/master.html:64
msgid "Apply For a new Group"
msgstr ""
#: fas/templates/master.html:65
#: fas/templates/master.html:59
msgid "News"
msgstr ""
#: fas/templates/master.html:69
#: fas/templates/master.html:62
msgid "User List"
msgstr ""
#: fas/templates/master.html:63
msgid "New Group"
msgstr ""
#: fas/templates/master.html:65
msgid "Apply For a new Group"
msgstr ""
#: fas/templates/master.html:66
msgid "Group List"
msgstr ""
#: fas/templates/master.html:70
msgid "Locale:"
msgstr ""
#: fas/templates/master.html:71
#: fas/templates/master.html:72
msgid "OK"
msgstr ""
#: fas/templates/master.html:83
#: fas/templates/master.html:84
msgid "About"
msgstr ""
#: fas/templates/master.html:84
#: fas/templates/master.html:85
msgid "Contact Us"
msgstr ""
#: fas/templates/master.html:85
#: fas/templates/master.html:86
msgid "Legal & Privacy"
msgstr ""
#: fas/templates/master.html:90
#: fas/templates/master.html:91
msgid ""
"Copyright © 2007 Red Hat, Inc. and others. All Rights Reserved. Please "
"send any comments or corrections to the <a "
"href=\"mailto:webmaster@fedoraproject.org\">websites team</a>."
msgstr ""
#: fas/templates/master.html:93
#: fas/templates/master.html:94
msgid ""
"The Fedora Project is maintained and driven by the community and "
"sponsored by Red Hat. This is a community maintained site. Red Hat is "
@ -719,7 +783,7 @@ msgstr ""
msgid "Fedora Contributor License Agreement"
msgstr ""
#: fas/templates/cla/index.html:12
#: fas/templates/cla/index.html:13
msgid ""
"There are two ways to sign the CLA. Most users will want to do a signed "
"CLA as it will promote them to a full contributor in Fedora. The click-"
@ -729,17 +793,11 @@ msgid ""
" Acceptance Hierarchies</a> for more information."
msgstr ""
#: fas/templates/cla/index.html:15 fas/templates/user/list.html:42
#: fas/templates/user/view.html:30
msgid "Signed CLA"
#: fas/templates/cla/index.html:18
msgid "Sign Contributor License Agreement (CLA)"
msgstr ""
#: fas/templates/cla/index.html:16 fas/templates/user/list.html:43
#: fas/templates/user/view.html:31
msgid "Click-through CLA"
msgstr ""
#: fas/templates/cla/index.html:19
#: fas/templates/cla/index.html:23
#, python-format
msgid "You have already sucessfully signed the <a href=\"%s\">CLA</a>."
msgstr ""
@ -783,12 +841,12 @@ msgid "Group Owner:"
msgstr ""
#: fas/templates/group/edit.html:25 fas/templates/group/new.html:29
#: fas/templates/group/view.html:38
#: fas/templates/group/view.html:39
msgid "Needs Sponsor:"
msgstr ""
#: fas/templates/group/edit.html:30 fas/templates/group/new.html:33
#: fas/templates/group/view.html:42
#: fas/templates/group/view.html:43
msgid "Self Removal:"
msgstr ""
@ -800,7 +858,7 @@ msgstr ""
msgid "Group Join Message:"
msgstr ""
#: fas/templates/group/edit.html:44 fas/templates/user/edit.html:73
#: fas/templates/group/edit.html:44 fas/templates/user/edit.html:74
msgid "Save!"
msgstr ""
@ -871,7 +929,6 @@ msgid "Approved"
msgstr ""
#: fas/templates/group/list.html:45 fas/templates/group/view.html:21
#: fas/templates/user/view.html:57
msgid "Unapproved"
msgstr ""
@ -891,7 +948,7 @@ msgstr ""
msgid "Must Belong To:"
msgstr ""
#: fas/templates/group/new.html:41 fas/templates/group/view.html:46
#: fas/templates/group/new.html:41 fas/templates/group/view.html:47
msgid "Join Message:"
msgstr ""
@ -911,91 +968,92 @@ msgstr ""
msgid "Remove me"
msgstr ""
#: fas/templates/group/view.html:31 fas/templates/user/view.html:13
#: fas/templates/group/view.html:32 fas/templates/user/view.html:13
msgid "(edit)"
msgstr ""
#: fas/templates/group/view.html:34 fas/templates/openid/id.html:16
#: fas/templates/group/view.html:35 fas/templates/openid/id.html:16
msgid "Name:"
msgstr ""
#: fas/templates/group/view.html:35
#: fas/templates/group/view.html:36
msgid "Description:"
msgstr ""
#: fas/templates/group/view.html:36
#: fas/templates/group/view.html:37
msgid "Owner:"
msgstr ""
#: fas/templates/group/view.html:37
#: fas/templates/group/view.html:38
msgid "Type:"
msgstr ""
#: fas/templates/group/view.html:39 fas/templates/group/view.html:43
#: fas/templates/group/view.html:40 fas/templates/group/view.html:44
msgid "Yes"
msgstr ""
#: fas/templates/group/view.html:40 fas/templates/group/view.html:44
#: fas/templates/group/view.html:41 fas/templates/group/view.html:45
msgid "No"
msgstr ""
#: fas/templates/group/view.html:47
#: fas/templates/group/view.html:48
msgid "Prerequisite:"
msgstr ""
#: fas/templates/group/view.html:50
#: fas/templates/group/view.html:51
msgid "Created:"
msgstr ""
#: fas/templates/group/view.html:58
#: fas/templates/group/view.html:59
msgid "Members"
msgstr ""
#: fas/templates/group/view.html:62 fas/templates/user/list.html:27
#: fas/templates/group/view.html:63 fas/templates/user/list.html:27
msgid "Username"
msgstr ""
#: fas/templates/group/view.html:63 fas/templates/group/view.html:83
#: fas/templates/group/view.html:64 fas/templates/group/view.html:85
msgid "Sponsor"
msgstr ""
#: fas/templates/group/view.html:64
#: fas/templates/group/view.html:65
msgid "Date Added"
msgstr ""
#: fas/templates/group/view.html:65
#: fas/templates/group/view.html:66
msgid "Date Approved"
msgstr ""
#: fas/templates/group/view.html:66
#: fas/templates/group/view.html:67
msgid "Approval"
msgstr ""
#: fas/templates/group/view.html:67
#: fas/templates/group/view.html:68
msgid "Role Type"
msgstr ""
#: fas/templates/group/view.html:68
#: fas/templates/group/view.html:69
msgid "Action"
msgstr ""
#: fas/templates/group/view.html:74
#: fas/templates/group/view.html:75 fas/templates/group/view.html:78
#: fas/templates/user/view.html:57
msgid "None"
msgstr ""
#: fas/templates/group/view.html:84
#: fas/templates/group/view.html:87
msgid "Approve"
msgstr ""
#: fas/templates/group/view.html:87
#: fas/templates/group/view.html:91
msgid "Remove"
msgstr ""
#: fas/templates/group/view.html:90
#: fas/templates/group/view.html:95
msgid "Upgrade"
msgstr ""
#: fas/templates/group/view.html:93
#: fas/templates/group/view.html:99
msgid "Downgrade"
msgstr ""
@ -1080,11 +1138,11 @@ msgstr ""
msgid "Locale"
msgstr ""
#: fas/templates/user/edit.html:68
#: fas/templates/user/edit.html:69
msgid "Comments"
msgstr ""
#: fas/templates/user/edit.html:74
#: fas/templates/user/edit.html:75
msgid "Cancel"
msgstr ""
@ -1100,6 +1158,14 @@ msgstr ""
msgid "Account Status"
msgstr ""
#: fas/templates/user/list.html:42 fas/templates/user/view.html:30
msgid "Signed CLA"
msgstr ""
#: fas/templates/user/list.html:43 fas/templates/user/view.html:31
msgid "Click-through CLA"
msgstr ""
#: fas/templates/user/list.html:44 fas/templates/user/view.html:32
msgid "Not Done"
msgstr ""
@ -1112,15 +1178,7 @@ msgstr ""
msgid "Email:"
msgstr ""
#: fas/templates/user/new.html:31 fas/templates/user/view.html:22
msgid "Telephone Number:"
msgstr ""
#: fas/templates/user/new.html:35 fas/templates/user/view.html:23
msgid "Postal Address:"
msgstr ""
#: fas/templates/user/new.html:39
#: fas/templates/user/new.html:38
msgid "Sign up!"
msgstr ""
@ -1170,6 +1228,14 @@ msgstr ""
msgid "PGP Key:"
msgstr ""
#: fas/templates/user/view.html:22
msgid "Telephone Number:"
msgstr ""
#: fas/templates/user/view.html:23
msgid "Postal Address:"
msgstr ""
#: fas/templates/user/view.html:24
msgid "Comments:"
msgstr ""