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 \ yum install git-core postgresql-plpython postgresql-server postgresql-python \
python-TurboMail TurboGears pygpgme python-sqlalchemy python-genshi \ 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 # 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 base_url_filter.base_url = "http://localhost:8080/fas" # Change the port if
# you changed server.socket_port above. # 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:: You should then be able to start the server and test things out::
./start-fas.py ./start-fas.py
# browse to http://localhost:8080/fas/ # 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:: in, then run::
tg-admin shell tg-admin shell
------- --------------------
Enabling Local Users Enabling Local Users
------- --------------------
* THIS IS EXPERIMENTAL * * THIS IS EXPERIMENTAL *
To allow local users to log in to your system, first enable fas via the To allow local users to log in to your system, first enable fas via the
@ -125,3 +128,17 @@ example:
getent passwd getent passwd
getent group 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,6 +162,7 @@ class MakeShellAccounts(BaseClient):
usernames = {} usernames = {}
for person in people: for person in people:
uid = person['id'] uid = person['id']
if self.is_valid_user(uid):
username = person['username'] username = person['username']
usernames[uid] = username usernames[uid] = username
file.write("=%i %s:x:%i:\n" % (uid, username, uid)) file.write("=%i %s:x:%i:\n" % (uid, username, uid))
@ -181,9 +182,9 @@ class MakeShellAccounts(BaseClient):
except KeyError: except KeyError:
''' No users exist in the group ''' ''' No users exist in the group '''
pass pass
file.write("=%i %s:x:%i:%s\n" % (gid, 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, self.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, self.memberships)) file.write(".%s %s:x:%i:%s\n" % (name, name, gid, memberships))
i = i + 1 i = i + 1
file.close() file.close()

View file

@ -7,10 +7,9 @@
#mail.server = 'bastion.fedora.phx.redhat.com' #mail.server = 'bastion.fedora.phx.redhat.com'
#base_url_filter.base_url = "http://192.168.2.101:8080" #base_url_filter.base_url = "http://192.168.2.101:8080"
fas.url = 'http://localhost:8088/fas/'
mail.on = True mail.on = True
mail.server = 'bastion.fedora.phx.redhat.com' mail.server = 'localhost'
mail.testmode = True #mail.testmode = True
mail.debug = False mail.debug = False
mail.encoding = 'utf-8' mail.encoding = 'utf-8'
@ -53,9 +52,9 @@ autoreload.package="fas"
# unexpected parameter. False by default # unexpected parameter. False by default
tg.strict_parameters = True tg.strict_parameters = True
server.webpath='/fas' server.webpath='/accounts'
base_url_filter.on=True 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 # Make the session cookie only return to the host over an SSL link
# Disabled for testing. # 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 Returns True if the user is an approved member of a group
''' '''
try: if group in person.approved_memberships:
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':
return True return True
else: else:
return False return False
@ -165,7 +158,7 @@ def canApplyGroup(person, group, applicant):
pass pass
else: else:
print "GOT HERE, prereq: %s" % prerequisite 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 return False
# A user can apply themselves, and FAS admins can apply other people. # A user can apply themselves, and FAS admins can apply other people.
@ -173,7 +166,7 @@ def canApplyGroup(person, group, applicant):
canAdminGroup(person, group): canAdminGroup(person, group):
return True return True
else: 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 return False
def canSponsorUser(person, group, target): def canSponsorUser(person, group, target):
@ -208,20 +201,18 @@ def canUpgradeUser(person, group, target):
''' '''
Returns True if the user can upgrade target in the group Returns True if the user can upgrade target in the group
''' '''
if isApproved(person, group):
# Group admins can upgrade anybody. # Group admins can upgrade anybody.
# The controller should handle the case where the target # The controller should handle the case where the target
# is already a group admin. # is already a group admin.
if canAdminGroup(person, group): if canAdminGroup(person, group):
return True return True
# Sponsors can only upgrade non-sponsors (i.e. normal users) # 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 \ elif canSponsorGroup(person, group) and \
not canSponsorGroup(target, group): not canSponsorGroup(target, group):
return True return True
else: else:
return False return False
else:
return False
def canDowngradeUser(person, group, target): def canDowngradeUser(person, group, target):
''' '''

View file

@ -53,12 +53,15 @@ class CLA(controllers.Controller):
turbogears.redirect('/user/edit/%s' % username) turbogears.redirect('/user/edit/%s' % username)
if type == 'click': if type == 'click':
if signedCLAPrivs(person): # Disable click-through CLA for now
turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.')) #if signedCLAPrivs(person):
turbogears.redirect('/cla/') # turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.'))
return dict() # turbogears.redirect('/cla/')
if clickedCLAPrivs(person): # return dict()
turbogears.flash(_('You have already completed the Click-through CLA.')) #if clickedCLAPrivs(person):
# turbogears.flash(_('You have already completed the Click-through CLA.'))
# turbogears.redirect('/cla/')
# return dict()
turbogears.redirect('/cla/') turbogears.redirect('/cla/')
return dict() return dict()
elif type == 'sign': elif type == 'sign':
@ -99,12 +102,18 @@ class CLA(controllers.Controller):
data = StringIO.StringIO(signature.file.read()) data = StringIO.StringIO(signature.file.read())
plaintext = StringIO.StringIO() plaintext = StringIO.StringIO()
verified = False verified = False
try: keyid = re.sub('\s', '', person.gpg_keyid)
subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', person.gpg_keyid]) ret = subprocess.call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
except subprocess.CalledProcessError: if ret != 0:
turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net")) turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
turbogears.redirect('/cla/view/sign') turbogears.redirect('/cla/view/sign')
return dict() 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: else:
try: try:
sigs = ctx.verify(data, None, plaintext) sigs = ctx.verify(data, None, plaintext)
@ -116,7 +125,7 @@ class CLA(controllers.Controller):
if len(sigs): if len(sigs):
sig = sigs[0] sig = sigs[0]
# This might still assume a full fingerprint. # 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 fpr = key.subkeys[0].fpr
if sig.fpr != fpr: if sig.fpr != fpr:
turbogears.flash(_("Your signature's fingerprint did not match the fingerprint registered in FAS.")) turbogears.flash(_("Your signature's fingerprint did not match the fingerprint registered in FAS."))
@ -125,7 +134,7 @@ class CLA(controllers.Controller):
emails = []; emails = [];
for uid in key.uids: for uid in key.uids:
emails.extend([uid.email]) emails.extend([uid.email])
if person.emails['cla'].email in emails: if person.emails['primary'].email in emails:
verified = True verified = True
else: else:
turbogears.flash(_('Your key did not match your email.')) turbogears.flash(_('Your key did not match your email.'))
@ -167,13 +176,15 @@ class CLA(controllers.Controller):
person.remove(cilckgroup, person) person.remove(cilckgroup, person)
except: except:
pass 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.flash(_("You have successfully signed the CLA. You are now in the '%s' group.") % group.name)
turbogears.redirect('/cla/') turbogears.redirect('/cla/')
return dict() return dict()
@identity.require(turbogears.identity.not_anonymous()) @identity.require(turbogears.identity.not_anonymous())
@error_handler(error) @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): def click(self, agree):
'''Click-through CLA''' '''Click-through CLA'''
username = turbogears.identity.current.user_name 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 # The commented out values below are the defaults
# Database values
sqlalchemy.convert_unicode=True
admingroup = 'accounts' admingroup = 'accounts'
# VIEW # VIEW
@ -151,9 +154,11 @@ identity.saprovider.model.group="fas.model.Groups"
# identity.saprovider.encryption_algorithm=None # identity.saprovider.encryption_algorithm=None
accounts_mail = "accounts@fedoraproject.org" accounts_mail = "accounts@fedoraproject.org"
#email_host = "fedoraproject.org"
email_host = "publictest10.fedoraproject.org"
gpgexec = "/usr/bin/gpg" 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_fingerprint = "C199 1E25 D00A D200 2D2E 54D1 BF7F 1647 C54E 8410"
gpg_passphrase = "m00!s@ysth3c0w" gpg_passphrase = "m00!s@ysth3c0w"
gpg_keyserver = "hkp://subkeys.pgp.net" gpg_keyserver = "hkp://subkeys.pgp.net"
@ -168,7 +173,7 @@ openidstore = "/var/tmp/fas/openid"
openssl_digest = "md5" openssl_digest = "md5"
openssl_expire = 31536000 # 60*60*24*365 = 1 year 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_c = "US"
openssl_st = "North Carolina" openssl_st = "North Carolina"
openssl_l = "Raleigh" openssl_l = "Raleigh"

View file

@ -13,6 +13,7 @@ from fas.group import Group
from fas.cla import CLA from fas.cla import CLA
from fas.json_request import JsonRequest from fas.json_request import JsonRequest
from fas.help import Help from fas.help import Help
from fas.auth import *
#from fas.openid_fas import OpenID #from fas.openid_fas import OpenID
import os import os
@ -28,9 +29,14 @@ turbogears.view.variable_providers.append(add_custom_stdvars)
def get_locale(locale=None): def get_locale(locale=None):
if locale: if locale:
return 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) person = People.by_username(turbogears.identity.current.user_name)
return person.locale return person.locale or 'C'
else: else:
return turbogears.i18n.utils._get_locale() return turbogears.i18n.utils._get_locale()
@ -58,13 +64,24 @@ class Root(controllers.RootController):
@expose(template="fas.templates.welcome", allow_json=True) @expose(template="fas.templates.welcome", allow_json=True)
def index(self): def index(self):
if turbogears.identity.not_anonymous(): 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') turbogears.redirect('/home')
return dict(now=time.ctime()) return dict(now=time.ctime())
@expose(template="fas.templates.home") @expose(template="fas.templates.home", allow_json=True)
@identity.require(identity.not_anonymous()) @identity.require(identity.not_anonymous())
def home(self): 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") @expose(template="fas.templates.about")
def about(self): def about(self):
@ -93,7 +110,7 @@ class Root(controllers.RootController):
# is better. # is better.
return dict(user = identity.current.user) return dict(user = identity.current.user)
if not forward_url: if not forward_url:
forward_url = config.get('base_url_filter.base_url') + '/' forward_url = '/'
raise redirect(forward_url) raise redirect(forward_url)
forward_url=None forward_url=None
@ -107,7 +124,7 @@ class Root(controllers.RootController):
"this resource.") "this resource.")
else: else:
msg=_("Please log in.") msg=_("Please log in.")
forward_url= request.headers.get("Referer", "/") forward_url= '/'
### FIXME: Is it okay to get rid of this? ### FIXME: Is it okay to get rid of this?
#cherrypy.response.status=403 #cherrypy.response.status=403
@ -125,7 +142,7 @@ class Root(controllers.RootController):
# redirect to a page. Returning the logged in identity # redirect to a page. Returning the logged in identity
# is better. # is better.
return dict(status=True) return dict(status=True)
raise redirect(request.headers.get("Referer", "/")) raise redirect('/')
@expose() @expose()
def language(self, locale): def language(self, locale):

View file

@ -4,8 +4,8 @@ from turbogears.database import session
import cherrypy import cherrypy
import fas
from fas.auth import * from fas.auth import *
from fas.user import KnownUser from fas.user import KnownUser
import re import re
@ -37,7 +37,7 @@ class GroupCreate(validators.Schema):
name = validators.All( name = validators.All(
UnknownGroup, UnknownGroup,
validators.String(max=32, min=3), validators.String(max=32, min=3),
validators.Regex(regex='^[a-z][a-z0-9]+$'), validators.Regex(regex='^[a-z0-9\-]+$'),
) )
display_name = validators.NotEmpty display_name = validators.NotEmpty
owner = KnownUser owner = KnownUser
@ -165,8 +165,8 @@ class Group(controllers.Controller):
group.display_name = display_name group.display_name = display_name
group.owner_id = person_owner.id group.owner_id = person_owner.id
group.group_type = group_type group.group_type = group_type
group.needs_sponsor = needs_sponsor group.needs_sponsor = bool(needs_sponsor)
group.user_can_remove = user_can_remove group.user_can_remove = bool(user_can_remove)
if prerequisite: if prerequisite:
prerequisite = Groups.by_name(prerequisite) prerequisite = Groups.by_name(prerequisite)
group.prerequisite = prerequisite group.prerequisite = prerequisite
@ -224,8 +224,8 @@ class Group(controllers.Controller):
group.display_name = display_name group.display_name = display_name
group.owner = owner group.owner = owner
group.group_type = group_type group.group_type = group_type
group.needs_sponsor = needs_sponsor group.needs_sponsor = bool(needs_sponsor)
group.user_can_remove = user_can_remove group.user_can_remove = bool(user_can_remove)
if prerequisite: if prerequisite:
prerequisite = Groups.by_name(prerequisite) prerequisite = Groups.by_name(prerequisite)
group.prerequisite = prerequisite group.prerequisite = prerequisite
@ -279,15 +279,16 @@ class Group(controllers.Controller):
else: else:
try: try:
target.apply(group, person) target.apply(group, person)
except: # TODO: More specific exception here. except fas.ApplyError, e:
turbogears.flash(_('%(user)s has already applied to %(group)s!') % \ turbogears.flash(_('%(user)s could not apply to %(group)s: %(error)s') % \
{'user': target.username, 'group': group.name}) {'user': target.username, 'group': group.name, 'error': e})
turbogears.redirect('/group/view/%s' % group.name)
else: else:
import turbomail import turbomail
# TODO: How do we handle gettext calls for these kinds of emails? # 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 # 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}) "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) 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: else:
try: try:
target.sponsor(group, person) target.sponsor(group, person)
except: except fas.SponsorError, e:
turbogears.flash(_("'%s' could not be sponsored!") % target.username) 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) turbogears.redirect('/group/view/%s' % group.name)
else: else:
import turbomail import turbomail
@ -335,7 +337,7 @@ propagate into the e-mail aliases and CVS repository within an hour.
%(joinmsg)s %(joinmsg)s
''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg} ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg}
turbomail.enqueue(message) 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) turbogears.redirect('/group/view/%s' % group.name)
return dict() return dict()
@ -358,9 +360,9 @@ propagate into the e-mail aliases and CVS repository within an hour.
else: else:
try: try:
target.remove(group, target) target.remove(group, target)
except KeyError: except fas.RemoveError, e:
turbogears.flash(_('%(name)s could not be removed from %(group)s!') % \ turbogears.flash(_("%(user)s could not be removed from %(group)s: %(error)s") % \
{'name': target.username, 'group': group.name}) {'user': target.username, 'group': group.name, 'error': e})
turbogears.redirect('/group/view/%s' % group.name) turbogears.redirect('/group/view/%s' % group.name)
else: else:
import turbomail import turbomail
@ -372,7 +374,7 @@ immediately for new operations, and should propagate into the e-mail
aliases within an hour. aliases within an hour.
''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email} ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email}
turbomail.enqueue(message) 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}) {'name': target.username, 'group': group.name})
turbogears.redirect('/group/view/%s' % group.name) turbogears.redirect('/group/view/%s' % group.name)
return dict() return dict()
@ -395,11 +397,9 @@ aliases within an hour.
else: else:
try: try:
target.upgrade(group, person) target.upgrade(group, person)
except TypeError, e: except fas.UpgradeError, e:
turbogears.flash(e) turbogears.flash(_('%(name)s could not be upgraded in %(group)s: %(error)s') % \
turbogears.redirect('/group/view/%s' % group.name) {'name': target.username, 'group': group.name, 'error': e})
except:
turbogears.flash(_('%(name)s could not be upgraded!') % {'name' : target.username})
turbogears.redirect('/group/view/%s' % group.name) turbogears.redirect('/group/view/%s' % group.name)
else: else:
import turbomail import turbomail
@ -436,8 +436,9 @@ into the e-mail aliases within an hour.
else: else:
try: try:
target.downgrade(group, person) target.downgrade(group, person)
except: except fas.DowngradeError, e:
turbogears.flash(_('%(username)s could not be downgraded!') % {'username': target.username}) 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) turbogears.redirect('/group/view/%s' % group.name)
else: else:
import turbomail import turbomail
@ -469,7 +470,7 @@ into the e-mail aliases within an hour.
turbogears.redirect('/group/list') turbogears.redirect('/group/list')
return dict() return dict()
else: else:
return dict(groups=groups) return dict(group=group)
@identity.require(identity.not_anonymous()) @identity.require(identity.not_anonymous())
@validate(validators=GroupInvite()) @validate(validators=GroupInvite())

View file

@ -16,6 +16,14 @@ class Help(controllers.Controller):
'user_comments': ['Comments (Optional)', '<p>Misc comments about yourself.</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_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_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): 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 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. # Bind us to the database defined in the config file.
get_engine() get_engine()
@ -121,6 +121,9 @@ class People(SABase):
''' '''
Apply a person to a group Apply a person to a group
''' '''
if group in cls.memberships:
raise fas.ApplyError, _('user is already in this group')
else:
role = PersonRoles() role = PersonRoles()
role.role_status = 'unapproved' role.role_status = 'unapproved'
role.role_type = 'user' role.role_type = 'user'
@ -131,8 +134,8 @@ class People(SABase):
''' '''
Approve a person in a group - requester for logging purposes Approve a person in a group - requester for logging purposes
''' '''
if group in cls.approved_memberships: if group not in cls.unapproved_memberships:
raise '%s is already approved in %s' % (cls.username, group.name) raise fas.ApproveError, _('user is not an unapproved member')
else: else:
role = PersonRoles.query.filter_by(member=cls, group=group).one() role = PersonRoles.query.filter_by(member=cls, group=group).one()
role.role_status = 'approved' role.role_status = 'approved'
@ -142,11 +145,11 @@ class People(SABase):
Upgrade a user in a group - requester for logging purposes Upgrade a user in a group - requester for logging purposes
''' '''
if not group in cls.memberships: 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: else:
role = PersonRoles.query.filter_by(member=cls, group=group).one() role = PersonRoles.query.filter_by(member=cls, group=group).one()
if role.role_type == 'administrator': 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': elif role.role_type == 'sponsor':
role.role_type = 'administrator' role.role_type = 'administrator'
elif role.role_type == 'user': elif role.role_type == 'user':
@ -157,11 +160,11 @@ class People(SABase):
Downgrade a user in a group - requester for logging purposes Downgrade a user in a group - requester for logging purposes
''' '''
if not group in cls.memberships: 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: else:
role = PersonRoles.query.filter_by(member=cls, group=group).one() role = PersonRoles.query.filter_by(member=cls, group=group).one()
if role.role_type == 'user': 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': elif role.role_type == 'sponsor':
role.role_type = 'user' role.role_type = 'user'
elif role.role_type == 'administrator': elif role.role_type == 'administrator':
@ -169,20 +172,19 @@ class People(SABase):
def sponsor(cls, group, requester): def sponsor(cls, group, requester):
# If we want to do logging, this might be the place. # If we want to do logging, this might be the place.
if not group in cls.memberships: if not group in cls.unapproved_memberships:
raise '%s not a member of %s' % (group.name, cls.memberships) raise fas.SponsorError, _('user is not an unapproved member')
role = PersonRoles.query.filter_by(member=cls, group=group).one() role = PersonRoles.query.filter_by(member=cls, group=group).one()
role.role_status = 'approved' role.role_status = 'approved'
role.sponsor_id = requester.id role.sponsor_id = requester.id
role.approval = datetime.now(pytz.utc) role.approval = datetime.now(pytz.utc)
def remove(cls, group, requester): def remove(cls, group, requester):
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() role = PersonRoles.query.filter_by(member=cls, group=group).one()
try:
session.delete(role) session.delete(role)
except TypeError:
pass
# Handle somehow.
def __repr__(cls): def __repr__(cls):
return "User(%s,%s)" % (cls.username, cls.human_name) return "User(%s,%s)" % (cls.username, cls.human_name)

View file

@ -106,7 +106,7 @@ class OpenID(controllers.Controller):
@validate(validators=UserID()) @validate(validators=UserID())
def id(self, username): def id(self, username):
'''The "real" OpenID URL''' '''The "real" OpenID URL'''
person = Person.by_username(username) person = People.by_username(username)
server = config.get('base_url') + turbogears.url('/openid/server') server = config.get('base_url') + turbogears.url('/openid/server')
return dict(person=person, server=server) return dict(person=person, server=server)

View file

@ -25,7 +25,7 @@ a
margin-top: 35px; margin-top: 35px;
height: 70px; height: 70px;
line-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 #head h1
@ -34,7 +34,7 @@ a
float: left; float: left;
text-indent: -9999px; text-indent: -9999px;
overflow: hidden; overflow: hidden;
background: url(/fas/static/images/logo.png) 1ex 50% no-repeat; background: url(../images/logo.png) 1ex 50% no-repeat;
} }
#searchbox #searchbox
@ -65,7 +65,7 @@ a
{ {
height: 30px; height: 30px;
line-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; font-size: 1.6ex;
} }
@ -78,7 +78,7 @@ a
#topnav ul li #topnav ul li
{ {
display: inline; 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; padding-left: 3px;
} }
@ -106,7 +106,7 @@ a
right: 0; right: 0;
height: 35px; height: 35px;
line-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; font-size: 1.6ex;
} }
@ -139,7 +139,7 @@ a
#control ul li #control ul li
{ {
display: inline; 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 #control a
@ -150,14 +150,14 @@ a
#main #main
{ {
background: url(/fas/static/images/shadow.png) 0 0 repeat-x; background: url(../images/shadow.png) 0 0 repeat-x;
} }
#sidebar #sidebar
{ {
width: 22ex; width: 22ex;
float: left; 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; border: 1px solid #112233;
} }
@ -246,25 +246,25 @@ a
.account .account
{ {
padding-left: 30px; padding-left: 30px;
background: url(/fas/static/images/account.png) 0 68% no-repeat; background: url(../images/account.png) 0 68% no-repeat;
} }
.approved .approved
{ {
padding-left: 20px; padding-left: 20px;
background: url(/fas/static/images/approved.png) 0 68% no-repeat; background: url(../images/approved.png) 0 68% no-repeat;
} }
.unapproved .unapproved
{ {
padding-left: 20px; padding-left: 20px;
background: url(/fas/static/images/unapproved.png) 0 68% no-repeat; background: url(../images/unapproved.png) 0 68% no-repeat;
} }
.attn .attn
{ {
padding-left: 20px; padding-left: 20px;
background: url(/fas/static/images/attn.png) 0 68% no-repeat; background: url(../images/attn.png) 0 68% no-repeat;
} }
.roleslist .roleslist
@ -302,7 +302,7 @@ a
margin-top: 1ex; margin-top: 1ex;
padding-top: 1ex; padding-top: 1ex;
padding-left: 22px; 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 #rolespanel h4
@ -332,13 +332,13 @@ a
#rolespanel .tools li #rolespanel .tools li
{ {
padding-left: 22px; 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 #rolespanel .queue li
{ {
padding-left: 22px; 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 #rolespanel .queue strong
@ -352,7 +352,7 @@ a
clear: both; clear: both;
text-align: center; text-align: center;
padding: 15px 0 2.5ex; 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 #footer .copy, #footer .disclaimer
@ -364,7 +364,7 @@ a
{ {
padding-top: 3px; padding-top: 3px;
padding-bottom: 18px; 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; list-style: none;
} }
@ -389,7 +389,7 @@ a
.flash .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; border: 1px solid #CCBBAA;
padding: 1.5ex 15px 1.5ex 43px; padding: 1.5ex 15px 1.5ex 43px;
margin: 1ex 0; margin: 1ex 0;
@ -397,7 +397,7 @@ a
.help .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; border: 1px solid #CCBBAA;
padding: 1.5ex 15px 1.5ex 65px; padding: 1.5ex 15px 1.5ex 65px;
margin: 1ex 0; margin: 1ex 0;

View file

@ -717,7 +717,7 @@ HelpBalloonOptions.prototype = {
* to an existing element if you're using that as your anchoring icon. * to an existing element if you're using that as your anchoring icon.
* @var {Object} * @var {Object}
*/ */
icon: '/static/images/balloons/icon.gif', icon: '/accounts/static/images/balloons/icon.gif',
/** /**
* Alt text of the help icon * Alt text of the help icon
* @var {String} * @var {String}
@ -783,7 +783,7 @@ HelpBalloonOptions.prototype = {
* Clossing button image path * Clossing button image path
* @var {String} * @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. * Balloon image path prefix. There are 4 button images, numerically named, starting with 0.
* 0, 1 * 0, 1
@ -791,7 +791,7 @@ HelpBalloonOptions.prototype = {
* (the number indicates the corner opposite the anchor (the pointing direction) * (the number indicates the corner opposite the anchor (the pointing direction)
* @var {String} * @var {String}
*/ */
balloonPrefix: '/static/images/balloons/balloon-', balloonPrefix: '/accounts/static/images/balloons/balloon-',
/** /**
* The image filename suffix, including the file extension * The image filename suffix, including the file extension
* @var {String} * @var {String}

View file

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

View file

@ -9,6 +9,7 @@
<body> <body>
<h2>${_('Fedora Contributor License Agreement')}</h2> <h2>${_('Fedora Contributor License Agreement')}</h2>
<p> <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.'))} ${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> </p>
<br/> <br/>

View file

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

View file

@ -45,6 +45,7 @@
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('Unapproved')}</span> <span class="unapproved" py:if="group in person.unapproved_memberships">${_('Unapproved')}</span>
</a> </a>
<a py:if="group not in person.memberships" href="${tg.url('/group/apply/%s/%s' % (group.name, person.username))}"><span>${_('Apply')}</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> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -27,15 +27,15 @@
</div> </div>
<div class="field"> <div class="field">
<label for="needs_sponsor">${_('Needs Sponsor:')}</label> <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>
<div class="field"> <div class="field">
<label for="user_can_remove">${_('Self Removal:')}</label> <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>
<div class="field"> <div class="field">
<label for="prerequisite">${_('Must Belong To:')}</label> <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>
<div class="field"> <div class="field">
<label for="joinmsg">${_('Join Message:')}</label> <label for="joinmsg">${_('Join Message:')}</label>

View file

@ -28,6 +28,7 @@
</div> </div>
</form> </form>
<a py:if="group in person.memberships" href="${tg.url('/group/remove/%s/%s' % (group.name, person.username))}">${_('Remove me')}</a> <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> <h3>Group Details <a py:if="auth.canAdminGroup(person, group)" href="${tg.url('/group/edit/%s' % group.name)}">${_('(edit)')}</a></h3>
<div class="userbox"> <div class="userbox">
<dl> <dl>
@ -70,10 +71,11 @@
</thead> </thead>
<tr py:for="role in group.roles"> <tr py:for="role in group.roles">
<td><a href="${tg.url('/user/view/%s' % role.member.username)}">${role.member.username}</a></td> <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.sponsor'><a href="${tg.url('/user/view/%s' % role.sponsor.username)}">${role.sponsor.username}</a></td>
<td py:if='role.member.username == "None"'>${_('None')}</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.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_status}</td>
<td>${role.role_type}</td> <td>${role.role_type}</td>
<!-- This section includes all action items --> <!-- This section includes all action items -->
@ -81,16 +83,21 @@
<ul class="actions"> <ul class="actions">
<li py:if="group in role.member.unapproved_memberships"> <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> <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> <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>
<li py:if="auth.canRemoveUser(person, group, role.member)"> <li py:if="auth.canRemoveUser(person, group, role.member)">
<a href="${tg.url('/group/remove/%s/%s' % (group.name, role.member.username))}">${_('Remove')}</a> <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>
<li py:if="auth.canUpgradeUser(person, group, role.member)"> <li py:if="auth.canUpgradeUser(person, group, role.member)">
<a href="${tg.url('/group/upgrade/%s/%s' % (group.name, role.member.username))}">${_('Upgrade')}</a> <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>
<li py:if="auth.canDowngradeUser(person, group, role.member)"> <li py:if="auth.canDowngradeUser(person, group, role.member)">
<a href="${tg.url('/group/downgrade/%s/%s' % (group.name, role.member.username))}">${_('Downgrade')}</a> <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> </li>
</ul> </ul>
</td> </td>

View file

@ -7,5 +7,25 @@
<title>${_('Fedora Accounts System')}</title> <title>${_('Fedora Accounts System')}</title>
</head> </head>
<body> <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> </body>
</html> </html>

View file

@ -46,7 +46,7 @@
</div> </div>
<div id="control"> <div id="control">
<ul> <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('/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="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> <li py:if="tg.identity.anonymous"><a href="${tg.url('/login')}">${_('Log In')}</a></li>
@ -56,13 +56,14 @@
<div id="main"> <div id="main">
<div id="sidebar"> <div id="sidebar">
<ul> <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=''> <div py:if="not tg.identity.anonymous and 'accounts' in tg.identity.groups" py:strip=''>
<!-- TODO: Make these use auth.py --> <!-- 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('/group/new')}">${_('New Group')}</a></li>
<li><a href="${tg.url('/user/list')}">${_('User List')}</a></li>
</div> </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> <li><a href="http://fedoraproject.org/wiki/FWN/LatestIssue">${_('News')}</a></li>
</ul> </ul>
<div py:if="tg.identity.anonymous" id="language"> <div py:if="tg.identity.anonymous" id="language">
@ -74,14 +75,34 @@
</div> </div>
</div> </div>
<div id="content"> <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} ${tg_flash}
</div> </div>
<div py:replace="select('*|text()')" /> <div py:replace="select('*|text()')" />
</div> </div>
<div id="footer"> <div id="footer">
<ul id="footlinks"> <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/Communicate">${_('Contact Us')}</a></li>
<li><a href="http://fedoraproject.org/wiki/Legal">${_('Legal &amp; Privacy')}</a></li> <li><a href="http://fedoraproject.org/wiki/Legal">${_('Legal &amp; Privacy')}</a></li>
<!--<li><a href="/">Site Map</a></li>--> <!--<li><a href="/">Site Map</a></li>-->

View file

@ -12,13 +12,13 @@
<div class="field"> <div class="field">
<label for="human_name">${_('Human Name')}:</label> <label for="human_name">${_('Human Name')}:</label>
<input type="text" id="human_name" name="human_name" value="${target.human_name}" /> <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> </div>
<!--Need to figure out what the interface should be for emails. --> <!--Need to figure out what the interface should be for emails. -->
<div class="field"> <div class="field">
<label for="mail">${_('Email')}:</label> <label for="mail">${_('Email')}:</label>
<input type="text" id="email" name="email" value="${target.emails['primary'].email}" /> <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>
<!-- <div class="field"> <!-- <div class="field">
<label for="fedoraPersonBugzillaMail">${_('Bugzilla Email')}:</label> <label for="fedoraPersonBugzillaMail">${_('Bugzilla Email')}:</label>
@ -27,22 +27,22 @@
<div class="field"> <div class="field">
<label for="ircnick">${_('IRC Nick')}:</label> <label for="ircnick">${_('IRC Nick')}:</label>
<input type="text" id="ircnick" name="ircnick" value="${target.ircnick}" /> <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>
<div class="field"> <div class="field">
<label for="gpg_keyid">${_('PGP Key')}:</label> <label for="gpg_keyid">${_('PGP Key')}:</label>
<input type="text" id="gpg_keyid" name="gpg_keyid" value="${target.gpg_keyid}" /> <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>
<div class="field"> <div class="field">
<label for="telephone">${_('Telephone Number')}:</label> <label for="telephone">${_('Telephone Number')}:</label>
<input type="text" id="telephone" name="telephone" value="${target.telephone}" /> <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>
<div class="field"> <div class="field">
<label for="postal_address">${_('Postal Address')}:</label> <label for="postal_address">${_('Postal Address')}:</label>
<textarea id="postal_address" name="postal_address">${target.postal_address}</textarea> <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>
<div class="field"> <div class="field">
<label for="timezone">${_('Time Zone')}:</label> <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> <option py:for="tz in common_timezones" value="${tz}" py:attrs="{'selected': target.timezone == tz and 'selected' or None}">${tz}</option>
</select> </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>
<div class="field"> <div class="field">
<label for="locale">${_('Locale')}:</label> <label for="locale">${_('Locale')}:</label>
<input type="text" id="locale" name="locale" value="${target.locale}" /> <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"> <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> option py:for="locale in available_locales" value="${locale}" py:attrs="{'selected': target.locale == locale and 'selected' or None}">${locale}</option>
</select> </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>
<div class="field"> <div class="field">
<label for="comments ">${_('Comments')}:</label> <label for="comments ">${_('Comments')}:</label>
<textarea id="comments" name="comments">${target.comments}</textarea> <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>
<div class="field"> <div class="field">
<input type="submit" value="${_('Save!')}" /> <input type="submit" value="${_('Save!')}" />

View file

@ -19,18 +19,21 @@
<!--<dt>${_('Bugzilla Email:')}</dt><dd>${person.username}</dd>--> <!--<dt>${_('Bugzilla Email:')}</dt><dd>${person.username}</dd>-->
<dt>${_('IRC Nick:')}</dt><dd>${person.ircnick}&nbsp;</dd> <dt>${_('IRC Nick:')}</dt><dd>${person.ircnick}&nbsp;</dd>
<dt>${_('PGP Key:')}</dt><dd>${person.gpg_keyid}&nbsp;</dd> <dt>${_('PGP Key:')}</dt><dd>${person.gpg_keyid}&nbsp;</dd>
<dt>${_('Telephone Number:')}</dt><dd>${person.telephone}&nbsp;</dd> <py:if test="personal"><dt>${_('Telephone Number:')}</dt><dd>${person.telephone}&nbsp;</dd></py:if>
<dt>${_('Postal Address:')}</dt><dd>${person.postal_address}&nbsp;</dd> <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>${_('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> <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 class="approved">${_('Valid')}</span> <dt>${_('Account Status:')}</dt><dd>
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/user_account_status'});</script></dd> <span py:if="person.status == 'active'" class="approved">${_('Active')}</span>
<!-- cla = {None, 'signed', 'clicked'} --> <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> <dt>${_('CLA:')}</dt><dd>
<span py:if="cla == 'signed'" class="approved">${_('Signed CLA')}</span> <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="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> <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> </dl>
</div> </div>
<h3 py:if="personal">${_('Your Roles')}</h3> <h3 py:if="personal">${_('Your Roles')}</h3>
@ -54,7 +57,7 @@
<dt>${_('Status:')}</dt> <dt>${_('Status:')}</dt>
<dd> <dd>
<span class="approved" py:if="group in person.approved_memberships">${_('Approved')}</span> <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> </dd>
<dt>${_('Tools:')}</dt> <dt>${_('Tools:')}</dt>
<dd> <dd>

View file

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

View file

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

View file

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