Merge branch 'master' of ssh://git.fedorahosted.org/git/fedora-infrastructure
This commit is contained in:
commit
1f3e3993d4
31 changed files with 553 additions and 1005 deletions
23
fas/README
23
fas/README
|
@ -27,7 +27,7 @@ Before you can get started, make sure to have the following packages installed
|
|||
|
||||
yum install git-core postgresql-plpython postgresql-server postgresql-python \
|
||||
python-TurboMail TurboGears pygpgme python-sqlalchemy python-genshi \
|
||||
python-psycopg2 pytz
|
||||
python-psycopg2 pytz python-babel babel
|
||||
|
||||
# Note: on RHEL5 you need postgresql-pl instead of postgresql-plpython
|
||||
|
||||
|
@ -90,6 +90,9 @@ You'll need to edit dev.cfg and change the following lines::
|
|||
base_url_filter.base_url = "http://localhost:8080/fas" # Change the port if
|
||||
# you changed server.socket_port above.
|
||||
|
||||
You may also need to change some of the directories and settings in
|
||||
fas/config/app.cfg.
|
||||
|
||||
You should then be able to start the server and test things out::
|
||||
./start-fas.py
|
||||
# browse to http://localhost:8080/fas/
|
||||
|
@ -101,9 +104,9 @@ Make sure you're in the top level directory that start-fas.py and dev.cfg is
|
|||
in, then run::
|
||||
tg-admin shell
|
||||
|
||||
-------
|
||||
--------------------
|
||||
Enabling Local Users
|
||||
-------
|
||||
--------------------
|
||||
* THIS IS EXPERIMENTAL *
|
||||
|
||||
To allow local users to log in to your system, first enable fas via the
|
||||
|
@ -125,3 +128,17 @@ example:
|
|||
getent passwd
|
||||
getent group
|
||||
|
||||
------------
|
||||
Localization
|
||||
------------
|
||||
To generate the POT file (located in the po/ subdirectory), run the
|
||||
following from the top level directory:
|
||||
|
||||
pybabel extract -F pybabel.conf -o po/fas.pot .
|
||||
To add a language: tg-admin i18n add <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
|
||||
|
||||
|
|
|
@ -162,12 +162,13 @@ class MakeShellAccounts(BaseClient):
|
|||
usernames = {}
|
||||
for person in people:
|
||||
uid = person['id']
|
||||
username = person['username']
|
||||
usernames[uid] = username
|
||||
file.write("=%i %s:x:%i:\n" % (uid, username, uid))
|
||||
file.write("0%i %s:x:%i:\n" % (i, username, uid))
|
||||
file.write(".%s %s:x:%i:\n" % (username, username, uid))
|
||||
i = i + 1
|
||||
if self.is_valid_user(uid):
|
||||
username = person['username']
|
||||
usernames[uid] = username
|
||||
file.write("=%i %s:x:%i:\n" % (uid, username, uid))
|
||||
file.write("0%i %s:x:%i:\n" % (i, username, uid))
|
||||
file.write(".%s %s:x:%i:\n" % (username, username, uid))
|
||||
i = i + 1
|
||||
|
||||
for group in groups:
|
||||
gid = group['id']
|
||||
|
@ -181,9 +182,9 @@ class MakeShellAccounts(BaseClient):
|
|||
except KeyError:
|
||||
''' No users exist in the group '''
|
||||
pass
|
||||
file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, self.memberships))
|
||||
file.write("0%i %s:x:%i:%s\n" % (i, name, gid, self.memberships))
|
||||
file.write(".%s %s:x:%i:%s\n" % (name, name, gid, self.memberships))
|
||||
file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, memberships))
|
||||
file.write("0%i %s:x:%i:%s\n" % (i, name, gid, memberships))
|
||||
file.write(".%s %s:x:%i:%s\n" % (name, name, gid, memberships))
|
||||
i = i + 1
|
||||
|
||||
file.close()
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
#mail.server = 'bastion.fedora.phx.redhat.com'
|
||||
#base_url_filter.base_url = "http://192.168.2.101:8080"
|
||||
|
||||
fas.url = 'http://localhost:8088/fas/'
|
||||
mail.on = True
|
||||
mail.server = 'bastion.fedora.phx.redhat.com'
|
||||
mail.testmode = True
|
||||
mail.server = 'localhost'
|
||||
#mail.testmode = True
|
||||
mail.debug = False
|
||||
mail.encoding = 'utf-8'
|
||||
|
||||
|
@ -53,9 +52,9 @@ autoreload.package="fas"
|
|||
# unexpected parameter. False by default
|
||||
tg.strict_parameters = True
|
||||
|
||||
server.webpath='/fas'
|
||||
server.webpath='/accounts'
|
||||
base_url_filter.on=True
|
||||
base_url_filter.base_url = "http://localhost:8088/fas"
|
||||
base_url_filter.base_url = "https://publictest10.fedoraproject.org/accounts"
|
||||
|
||||
# Make the session cookie only return to the host over an SSL link
|
||||
# Disabled for testing.
|
||||
|
|
|
@ -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
|
|
@ -65,14 +65,7 @@ def isApproved(person, group):
|
|||
'''
|
||||
Returns True if the user is an approved member of a group
|
||||
'''
|
||||
try:
|
||||
role = PersonRoles.query.filter_by(group=group, member=person).one()
|
||||
except IndexError:
|
||||
''' Not in the group '''
|
||||
return False
|
||||
except InvalidRequestError:
|
||||
return False
|
||||
if role.role_status == 'approved':
|
||||
if group in person.approved_memberships:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@ -165,7 +158,7 @@ def canApplyGroup(person, group, applicant):
|
|||
pass
|
||||
else:
|
||||
print "GOT HERE, prereq: %s" % prerequisite
|
||||
turbogears.flash(_('%s membership required before application to this group is allowed' % prerequisite.name))
|
||||
turbogears.flash(_('%s membership required before application to this group is allowed') % prerequisite.name)
|
||||
return False
|
||||
# A user can apply themselves, and FAS admins can apply other people.
|
||||
|
||||
|
@ -173,7 +166,7 @@ def canApplyGroup(person, group, applicant):
|
|||
canAdminGroup(person, group):
|
||||
return True
|
||||
else:
|
||||
turbogears.flash(_('%s membership required before application to this group is allowed' % prerequisite.name))
|
||||
turbogears.flash(_('%s membership required before application to this group is allowed') % prerequisite.name)
|
||||
return False
|
||||
|
||||
def canSponsorUser(person, group, target):
|
||||
|
@ -208,18 +201,16 @@ def canUpgradeUser(person, group, target):
|
|||
'''
|
||||
Returns True if the user can upgrade target in the group
|
||||
'''
|
||||
if isApproved(person, group):
|
||||
# Group admins can upgrade anybody.
|
||||
# The controller should handle the case where the target
|
||||
# is already a group admin.
|
||||
if canAdminGroup(person, group):
|
||||
return True
|
||||
# Sponsors can only upgrade non-sponsors (i.e. normal users)
|
||||
elif canSponsorGroup(person, group) and \
|
||||
not canSponsorGroup(target, group):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
# Group admins can upgrade anybody.
|
||||
# The controller should handle the case where the target
|
||||
# is already a group admin.
|
||||
if canAdminGroup(person, group):
|
||||
return True
|
||||
# Sponsors can only upgrade non-sponsors (i.e. normal users)
|
||||
# TODO: Don't assume that canSponsorGroup means that the user is a sponsor
|
||||
elif canSponsorGroup(person, group) and \
|
||||
not canSponsorGroup(target, group):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
|
@ -53,14 +53,17 @@ class CLA(controllers.Controller):
|
|||
turbogears.redirect('/user/edit/%s' % username)
|
||||
|
||||
if type == 'click':
|
||||
if signedCLAPrivs(person):
|
||||
turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.'))
|
||||
turbogears.redirect('/cla/')
|
||||
return dict()
|
||||
if clickedCLAPrivs(person):
|
||||
turbogears.flash(_('You have already completed the Click-through CLA.'))
|
||||
turbogears.redirect('/cla/')
|
||||
return dict()
|
||||
# Disable click-through CLA for now
|
||||
#if signedCLAPrivs(person):
|
||||
# turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.'))
|
||||
# turbogears.redirect('/cla/')
|
||||
# return dict()
|
||||
#if clickedCLAPrivs(person):
|
||||
# turbogears.flash(_('You have already completed the Click-through CLA.'))
|
||||
# turbogears.redirect('/cla/')
|
||||
# return dict()
|
||||
turbogears.redirect('/cla/')
|
||||
return dict()
|
||||
elif type == 'sign':
|
||||
if signedCLAPrivs(person):
|
||||
turbogears.flash(_('You have already signed the CLA.'))
|
||||
|
@ -99,12 +102,18 @@ class CLA(controllers.Controller):
|
|||
data = StringIO.StringIO(signature.file.read())
|
||||
plaintext = StringIO.StringIO()
|
||||
verified = False
|
||||
try:
|
||||
subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', person.gpg_keyid])
|
||||
except subprocess.CalledProcessError:
|
||||
keyid = re.sub('\s', '', person.gpg_keyid)
|
||||
ret = subprocess.call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
|
||||
if ret != 0:
|
||||
turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
|
||||
turbogears.redirect('/cla/view/sign')
|
||||
return dict()
|
||||
#try:
|
||||
# subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
|
||||
#except subprocess.CalledProcessError:
|
||||
# turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
|
||||
# turbogears.redirect('/cla/view/sign')
|
||||
# return dict()
|
||||
else:
|
||||
try:
|
||||
sigs = ctx.verify(data, None, plaintext)
|
||||
|
@ -116,7 +125,7 @@ class CLA(controllers.Controller):
|
|||
if len(sigs):
|
||||
sig = sigs[0]
|
||||
# This might still assume a full fingerprint.
|
||||
key = ctx.get_key(re.sub('\s', '', person.gpg_keyid))
|
||||
key = ctx.get_key(keyid)
|
||||
fpr = key.subkeys[0].fpr
|
||||
if sig.fpr != fpr:
|
||||
turbogears.flash(_("Your signature's fingerprint did not match the fingerprint registered in FAS."))
|
||||
|
@ -125,7 +134,7 @@ class CLA(controllers.Controller):
|
|||
emails = [];
|
||||
for uid in key.uids:
|
||||
emails.extend([uid.email])
|
||||
if person.emails['cla'].email in emails:
|
||||
if person.emails['primary'].email in emails:
|
||||
verified = True
|
||||
else:
|
||||
turbogears.flash(_('Your key did not match your email.'))
|
||||
|
@ -167,13 +176,15 @@ class CLA(controllers.Controller):
|
|||
person.remove(cilckgroup, person)
|
||||
except:
|
||||
pass
|
||||
# TODO: Email legal-cla-archive@fedoraproject.org
|
||||
turbogears.flash(_("You have successfully signed the CLA. You are now in the '%s' group.") % group.name)
|
||||
turbogears.redirect('/cla/')
|
||||
return dict()
|
||||
|
||||
@identity.require(turbogears.identity.not_anonymous())
|
||||
@error_handler(error)
|
||||
@expose(template="fas.templates.cla.index")
|
||||
# Don't expose click-through CLA for now.
|
||||
#@expose(template="fas.templates.cla.index")
|
||||
def click(self, agree):
|
||||
'''Click-through CLA'''
|
||||
username = turbogears.identity.current.user_name
|
||||
|
|
|
@ -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
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
# The commented out values below are the defaults
|
||||
|
||||
# Database values
|
||||
sqlalchemy.convert_unicode=True
|
||||
|
||||
admingroup = 'accounts'
|
||||
|
||||
# VIEW
|
||||
|
@ -151,9 +154,11 @@ identity.saprovider.model.group="fas.model.Groups"
|
|||
# identity.saprovider.encryption_algorithm=None
|
||||
|
||||
accounts_mail = "accounts@fedoraproject.org"
|
||||
#email_host = "fedoraproject.org"
|
||||
email_host = "publictest10.fedoraproject.org"
|
||||
|
||||
gpgexec = "/usr/bin/gpg"
|
||||
gpghome = "/home/ricky/work/fedora/fedora-infrastructure/fas/gnupg"
|
||||
gpghome = "/srv/fedora-infrastructure/fas/gnupg"
|
||||
gpg_fingerprint = "C199 1E25 D00A D200 2D2E 54D1 BF7F 1647 C54E 8410"
|
||||
gpg_passphrase = "m00!s@ysth3c0w"
|
||||
gpg_keyserver = "hkp://subkeys.pgp.net"
|
||||
|
@ -168,7 +173,7 @@ openidstore = "/var/tmp/fas/openid"
|
|||
|
||||
openssl_digest = "md5"
|
||||
openssl_expire = 31536000 # 60*60*24*365 = 1 year
|
||||
openssl_ca_file = "/home/ricky/work/fedora/fedora-infrastructure/fas/ssl/ca-Upload"
|
||||
openssl_ca_file = "/srv/fedora-infrastructure/fas/ssl/ca-Upload"
|
||||
openssl_c = "US"
|
||||
openssl_st = "North Carolina"
|
||||
openssl_l = "Raleigh"
|
||||
|
|
|
@ -13,6 +13,7 @@ from fas.group import Group
|
|||
from fas.cla import CLA
|
||||
from fas.json_request import JsonRequest
|
||||
from fas.help import Help
|
||||
from fas.auth import *
|
||||
#from fas.openid_fas import OpenID
|
||||
|
||||
import os
|
||||
|
@ -28,9 +29,14 @@ turbogears.view.variable_providers.append(add_custom_stdvars)
|
|||
def get_locale(locale=None):
|
||||
if locale:
|
||||
return locale
|
||||
if turbogears.identity.current.user_name:
|
||||
username = None
|
||||
try:
|
||||
username = turbogears.identity.current.user_name
|
||||
except AttributeError:
|
||||
pass
|
||||
if username:
|
||||
person = People.by_username(turbogears.identity.current.user_name)
|
||||
return person.locale
|
||||
return person.locale or 'C'
|
||||
else:
|
||||
return turbogears.i18n.utils._get_locale()
|
||||
|
||||
|
@ -58,13 +64,24 @@ class Root(controllers.RootController):
|
|||
@expose(template="fas.templates.welcome", allow_json=True)
|
||||
def index(self):
|
||||
if turbogears.identity.not_anonymous():
|
||||
if 'tg_format' in request.params \
|
||||
and request.params['tg_format'] == 'json':
|
||||
# redirects don't work with JSON calls. This is a bit of a
|
||||
# hack until we can figure out something better.
|
||||
return dict()
|
||||
turbogears.redirect('/home')
|
||||
return dict(now=time.ctime())
|
||||
|
||||
@expose(template="fas.templates.home")
|
||||
@expose(template="fas.templates.home", allow_json=True)
|
||||
@identity.require(identity.not_anonymous())
|
||||
def home(self):
|
||||
return dict()
|
||||
user_name = turbogears.identity.current.user_name
|
||||
person = People.by_username(user_name)
|
||||
cla = None
|
||||
if signedCLAPrivs(person):
|
||||
cla = 'signed'
|
||||
|
||||
return dict(person=person, cla=cla)
|
||||
|
||||
@expose(template="fas.templates.about")
|
||||
def about(self):
|
||||
|
@ -93,7 +110,7 @@ class Root(controllers.RootController):
|
|||
# is better.
|
||||
return dict(user = identity.current.user)
|
||||
if not forward_url:
|
||||
forward_url = config.get('base_url_filter.base_url') + '/'
|
||||
forward_url = '/'
|
||||
raise redirect(forward_url)
|
||||
|
||||
forward_url=None
|
||||
|
@ -107,7 +124,7 @@ class Root(controllers.RootController):
|
|||
"this resource.")
|
||||
else:
|
||||
msg=_("Please log in.")
|
||||
forward_url= request.headers.get("Referer", "/")
|
||||
forward_url= '/'
|
||||
|
||||
### FIXME: Is it okay to get rid of this?
|
||||
#cherrypy.response.status=403
|
||||
|
@ -125,7 +142,7 @@ class Root(controllers.RootController):
|
|||
# redirect to a page. Returning the logged in identity
|
||||
# is better.
|
||||
return dict(status=True)
|
||||
raise redirect(request.headers.get("Referer", "/"))
|
||||
raise redirect('/')
|
||||
|
||||
@expose()
|
||||
def language(self, locale):
|
||||
|
|
|
@ -4,8 +4,8 @@ from turbogears.database import session
|
|||
|
||||
import cherrypy
|
||||
|
||||
import fas
|
||||
from fas.auth import *
|
||||
|
||||
from fas.user import KnownUser
|
||||
|
||||
import re
|
||||
|
@ -37,7 +37,7 @@ class GroupCreate(validators.Schema):
|
|||
name = validators.All(
|
||||
UnknownGroup,
|
||||
validators.String(max=32, min=3),
|
||||
validators.Regex(regex='^[a-z][a-z0-9]+$'),
|
||||
validators.Regex(regex='^[a-z0-9\-]+$'),
|
||||
)
|
||||
display_name = validators.NotEmpty
|
||||
owner = KnownUser
|
||||
|
@ -165,8 +165,8 @@ class Group(controllers.Controller):
|
|||
group.display_name = display_name
|
||||
group.owner_id = person_owner.id
|
||||
group.group_type = group_type
|
||||
group.needs_sponsor = needs_sponsor
|
||||
group.user_can_remove = user_can_remove
|
||||
group.needs_sponsor = bool(needs_sponsor)
|
||||
group.user_can_remove = bool(user_can_remove)
|
||||
if prerequisite:
|
||||
prerequisite = Groups.by_name(prerequisite)
|
||||
group.prerequisite = prerequisite
|
||||
|
@ -224,8 +224,8 @@ class Group(controllers.Controller):
|
|||
group.display_name = display_name
|
||||
group.owner = owner
|
||||
group.group_type = group_type
|
||||
group.needs_sponsor = needs_sponsor
|
||||
group.user_can_remove = user_can_remove
|
||||
group.needs_sponsor = bool(needs_sponsor)
|
||||
group.user_can_remove = bool(user_can_remove)
|
||||
if prerequisite:
|
||||
prerequisite = Groups.by_name(prerequisite)
|
||||
group.prerequisite = prerequisite
|
||||
|
@ -279,15 +279,16 @@ class Group(controllers.Controller):
|
|||
else:
|
||||
try:
|
||||
target.apply(group, person)
|
||||
except: # TODO: More specific exception here.
|
||||
turbogears.flash(_('%(user)s has already applied to %(group)s!') % \
|
||||
{'user': target.username, 'group': group.name})
|
||||
except fas.ApplyError, e:
|
||||
turbogears.flash(_('%(user)s could not apply to %(group)s: %(error)s') % \
|
||||
{'user': target.username, 'group': group.name, 'error': e})
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
else:
|
||||
import turbomail
|
||||
|
||||
# TODO: How do we handle gettext calls for these kinds of emails?
|
||||
# TODO: CC to right place, put a bit more thought into how to most elegantly do this
|
||||
message = turbomail.Message(config.get('accounts_mail'), '%s-sponsors@fedoraproject.org' % group.name, \
|
||||
# TODO: Maybe that @fedoraproject.org (and even -sponsors) should be configurable somewhere?
|
||||
message = turbomail.Message(config.get('accounts_mail'), '%(group)s-sponsors@%(host)s' % {'group': group.name, 'host': config.get('email_host')}, \
|
||||
"Fedora '%(group)s' sponsor needed for %(user)s" % {'user': target.username, 'group': group.name})
|
||||
url = config.get('base_url_filter.base_url') + turbogears.url('/group/edit/%s' % groupname)
|
||||
|
||||
|
@ -321,8 +322,9 @@ Please go to %(url)s to take action.
|
|||
else:
|
||||
try:
|
||||
target.sponsor(group, person)
|
||||
except:
|
||||
turbogears.flash(_("'%s' could not be sponsored!") % target.username)
|
||||
except fas.SponsorError, e:
|
||||
turbogears.flash(_("%(user)s could not be sponsored in %(group)s: %(error)s") % \
|
||||
{'user': target.username, 'group': group.name, 'error': e})
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
else:
|
||||
import turbomail
|
||||
|
@ -335,7 +337,7 @@ propagate into the e-mail aliases and CVS repository within an hour.
|
|||
%(joinmsg)s
|
||||
''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg}
|
||||
turbomail.enqueue(message)
|
||||
turbogears.flash(_("'%s' has been sponsored!") % person.human_name)
|
||||
turbogears.flash(_("'%s' has been sponsored!") % target.human_name)
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
return dict()
|
||||
|
||||
|
@ -358,9 +360,9 @@ propagate into the e-mail aliases and CVS repository within an hour.
|
|||
else:
|
||||
try:
|
||||
target.remove(group, target)
|
||||
except KeyError:
|
||||
turbogears.flash(_('%(name)s could not be removed from %(group)s!') % \
|
||||
{'name': target.username, 'group': group.name})
|
||||
except fas.RemoveError, e:
|
||||
turbogears.flash(_("%(user)s could not be removed from %(group)s: %(error)s") % \
|
||||
{'user': target.username, 'group': group.name, 'error': e})
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
else:
|
||||
import turbomail
|
||||
|
@ -372,7 +374,7 @@ immediately for new operations, and should propagate into the e-mail
|
|||
aliases within an hour.
|
||||
''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email}
|
||||
turbomail.enqueue(message)
|
||||
turbogears.flash(_('%(name)s has been removed from %(group)s!') % \
|
||||
turbogears.flash(_('%(name)s has been removed from %(group)s') % \
|
||||
{'name': target.username, 'group': group.name})
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
return dict()
|
||||
|
@ -395,11 +397,9 @@ aliases within an hour.
|
|||
else:
|
||||
try:
|
||||
target.upgrade(group, person)
|
||||
except TypeError, e:
|
||||
turbogears.flash(e)
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
except:
|
||||
turbogears.flash(_('%(name)s could not be upgraded!') % {'name' : target.username})
|
||||
except fas.UpgradeError, e:
|
||||
turbogears.flash(_('%(name)s could not be upgraded in %(group)s: %(error)s') % \
|
||||
{'name': target.username, 'group': group.name, 'error': e})
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
else:
|
||||
import turbomail
|
||||
|
@ -436,8 +436,9 @@ into the e-mail aliases within an hour.
|
|||
else:
|
||||
try:
|
||||
target.downgrade(group, person)
|
||||
except:
|
||||
turbogears.flash(_('%(username)s could not be downgraded!') % {'username': target.username})
|
||||
except fas.DowngradeError, e:
|
||||
turbogears.flash(_('%(name)s could not be downgraded in %(group)s: %(error)s') % \
|
||||
{'name': target.username, 'group': group.name, 'error': e})
|
||||
turbogears.redirect('/group/view/%s' % group.name)
|
||||
else:
|
||||
import turbomail
|
||||
|
@ -469,7 +470,7 @@ into the e-mail aliases within an hour.
|
|||
turbogears.redirect('/group/list')
|
||||
return dict()
|
||||
else:
|
||||
return dict(groups=groups)
|
||||
return dict(group=group)
|
||||
|
||||
@identity.require(identity.not_anonymous())
|
||||
@validate(validators=GroupInvite())
|
||||
|
|
|
@ -5,17 +5,25 @@ from turbogears.database import session
|
|||
from fas.auth import *
|
||||
|
||||
class Help(controllers.Controller):
|
||||
help = { 'none' : ['Error', '<p>We could not find that help item</p>'],
|
||||
'user_ircnick' : ['IRC Nick (Optional)', '<p>IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to</p>'],
|
||||
'user_primary_email' : ['Primary Email (Required)', '<p>This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded</p>'],
|
||||
'user_human_name' : ['Full Name (Required)', '<p>Your Human Name or "real life" name</p>'],
|
||||
'user_gpg_keyid' : ['GPG Key', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a> for more information</p>'],
|
||||
'user_telephone' : ['Telephone', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a></p>'],
|
||||
'user_postal_address': ['Postal Address', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. This should be a mailing address where you can be contacted. See our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a> about any concerns.</p>'],
|
||||
'user_timezone': ['Timezone (Optional)', '<p>Please specify the time zone you are in.</p>'],
|
||||
'user_comments': ['Comments (Optional)', '<p>Misc comments about yourself.</p>'],
|
||||
'user_account_status': ['Account Status', '<p>Shows account status, possible values include<ul><li>Valid</li><li>Disabled</li><li>Expired</li></ul></p>'],
|
||||
'user_cla' : ['CLA', '<p>In order to become a full Fedora contributor you must sign a <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">Contributor License Agreement</a>. This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a>.</p>'],
|
||||
help = { 'none' : ['Error', '<p>We could not find that help item</p>'],
|
||||
'user_ircnick' : ['IRC Nick (Optional)', '<p>IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to</p>'],
|
||||
'user_primary_email' : ['Primary Email (Required)', '<p>This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded</p>'],
|
||||
'user_human_name' : ['Full Name (Required)', '<p>Your Human Name or "real life" name</p>'],
|
||||
'user_gpg_keyid' : ['GPG Key', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a> for more information</p>'],
|
||||
'user_telephone' : ['Telephone', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a></p>'],
|
||||
'user_postal_address': ['Postal Address', '<p>Only required for users signing the <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">CLA</a>. This should be a mailing address where you can be contacted. See our <a href="http://fedoraproject.org/wiki/Legal/PrivacyPolicy">Privacy Policy</a> about any concerns.</p>'],
|
||||
'user_timezone': ['Timezone (Optional)', '<p>Please specify the time zone you are in.</p>'],
|
||||
'user_comments': ['Comments (Optional)', '<p>Misc comments about yourself.</p>'],
|
||||
'user_account_status': ['Account Status', '<p>Shows account status, possible values include<ul><li>Valid</li><li>Disabled</li><li>Expired</li></ul></p>'],
|
||||
'user_cla' : ['CLA', '<p>In order to become a full Fedora contributor you must sign a <a href="http://fedoraproject.org/wiki/Legal/Licenses/CLA">Contributor License Agreement</a>. This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the <a href="http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo">CLAHowTo</a>.</p>'],
|
||||
'user_locale': ['Locale', '<p>For non-english speaking peoples this allows individuals to select which locale they are in.</p>'],
|
||||
|
||||
'group_apply': ['Apply', '<p>Applying for a group is like applying for a job and it can certainly take a while to get in. Many groups have their own rules about how to actually get approved or sponsored. For more information on how the account system works see the <a href="../about">about page</a>.</p>'],
|
||||
'group_remove': ['Remove', '''<p>Removing a person from a group will cause that user to no longer be in the group. They will need to re-apply to get in. Admins can remove anyone, Sponsors can remove users, users can't remove anyone.</p>'''],
|
||||
'group_upgrade': ['Upgrade', '''<p>Upgrade a persons status in this group.<ul><li>from user -> to sponsor</li><li>From sponsor -> administrator</li><li>administrators cannot be upgraded beyond administrator</li></ul></p>'''],
|
||||
'group_downgrade': ['Downgrade', '''<p>Downgrade a persons status in the group.<ul><li>from administrator -> to sponsor</li><li>From sponsor -> user</li><li>users cannot be downgraded below user, you may want to remove them</li></ul></p>'''],
|
||||
'group_approve': ['Approve', '''<p>A sponsor or administrator can approve users to be in a group. Once the user has applied for the group, go to the group's page and click approve to approve the user.</p>'''],
|
||||
'group_sponsor': ['Sponsor', '''<p>A sponsor or administrator can sponsor users to be in a gruop. Once the user has applied for the group, go to the group's page and click approve to sponsor the user. Sponsorship of a user implies that you are approving a user and may mentor and answer their questions as they come up.</p>'''],
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
120
fas/fas/json.py
120
fas/fas/json.py
|
@ -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]
|
|
@ -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)
|
|
@ -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
|
|
@ -42,10 +42,10 @@ from turbogears.database import session
|
|||
|
||||
from turbogears import identity
|
||||
|
||||
from fas.json import SABase
|
||||
import turbogears
|
||||
|
||||
# Soon we'll use this instead:
|
||||
#from fedora.tg.json import SABase
|
||||
from fedora.tg.json import SABase
|
||||
import fas
|
||||
|
||||
# Bind us to the database defined in the config file.
|
||||
get_engine()
|
||||
|
@ -121,18 +121,21 @@ class People(SABase):
|
|||
'''
|
||||
Apply a person to a group
|
||||
'''
|
||||
role = PersonRoles()
|
||||
role.role_status = 'unapproved'
|
||||
role.role_type = 'user'
|
||||
role.member = cls
|
||||
role.group = group
|
||||
if group in cls.memberships:
|
||||
raise fas.ApplyError, _('user is already in this group')
|
||||
else:
|
||||
role = PersonRoles()
|
||||
role.role_status = 'unapproved'
|
||||
role.role_type = 'user'
|
||||
role.member = cls
|
||||
role.group = group
|
||||
|
||||
def approve(cls, group, requester):
|
||||
'''
|
||||
Approve a person in a group - requester for logging purposes
|
||||
'''
|
||||
if group in cls.approved_memberships:
|
||||
raise '%s is already approved in %s' % (cls.username, group.name)
|
||||
if group not in cls.unapproved_memberships:
|
||||
raise fas.ApproveError, _('user is not an unapproved member')
|
||||
else:
|
||||
role = PersonRoles.query.filter_by(member=cls, group=group).one()
|
||||
role.role_status = 'approved'
|
||||
|
@ -142,11 +145,11 @@ class People(SABase):
|
|||
Upgrade a user in a group - requester for logging purposes
|
||||
'''
|
||||
if not group in cls.memberships:
|
||||
raise '%s not a member of %s' % (group.name, cls.memberships)
|
||||
raise fas.UpgradeError, _('user is not a member')
|
||||
else:
|
||||
role = PersonRoles.query.filter_by(member=cls, group=group).one()
|
||||
if role.role_type == 'administrator':
|
||||
raise '%s is already an admin in %s' % (cls.username, group.name)
|
||||
raise fas.UpgradeError, _('administrators cannot be upgraded any further')
|
||||
elif role.role_type == 'sponsor':
|
||||
role.role_type = 'administrator'
|
||||
elif role.role_type == 'user':
|
||||
|
@ -157,11 +160,11 @@ class People(SABase):
|
|||
Downgrade a user in a group - requester for logging purposes
|
||||
'''
|
||||
if not group in cls.memberships:
|
||||
raise '%s not a member of %s' % (group.name, cls.memberships)
|
||||
raise fas.DowngradeError, _('user is not a member')
|
||||
else:
|
||||
role = PersonRoles.query.filter_by(member=cls, group=group).one()
|
||||
if role.role_type == 'user':
|
||||
raise '%s is already a user in %s, did you mean to remove?' % (cls.username, group.name)
|
||||
raise fas.DowngradeError, _('users cannot be downgraded any further')
|
||||
elif role.role_type == 'sponsor':
|
||||
role.role_type = 'user'
|
||||
elif role.role_type == 'administrator':
|
||||
|
@ -169,20 +172,19 @@ class People(SABase):
|
|||
|
||||
def sponsor(cls, group, requester):
|
||||
# If we want to do logging, this might be the place.
|
||||
if not group in cls.memberships:
|
||||
raise '%s not a member of %s' % (group.name, cls.memberships)
|
||||
if not group in cls.unapproved_memberships:
|
||||
raise fas.SponsorError, _('user is not an unapproved member')
|
||||
role = PersonRoles.query.filter_by(member=cls, group=group).one()
|
||||
role.role_status = 'approved'
|
||||
role.sponsor_id = requester.id
|
||||
role.approval = datetime.now(pytz.utc)
|
||||
|
||||
def remove(cls, group, requester):
|
||||
role = PersonRoles.query.filter_by(member=cls, group=group).one()
|
||||
try:
|
||||
if not group in cls.memberships:
|
||||
raise fas.RemoveError, _('user is not a member')
|
||||
else:
|
||||
role = PersonRoles.query.filter_by(member=cls, group=group).one()
|
||||
session.delete(role)
|
||||
except TypeError:
|
||||
pass
|
||||
# Handle somehow.
|
||||
|
||||
def __repr__(cls):
|
||||
return "User(%s,%s)" % (cls.username, cls.human_name)
|
||||
|
|
|
@ -106,7 +106,7 @@ class OpenID(controllers.Controller):
|
|||
@validate(validators=UserID())
|
||||
def id(self, username):
|
||||
'''The "real" OpenID URL'''
|
||||
person = Person.by_username(username)
|
||||
person = People.by_username(username)
|
||||
server = config.get('base_url') + turbogears.url('/openid/server')
|
||||
return dict(person=person, server=server)
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ a
|
|||
margin-top: 35px;
|
||||
height: 70px;
|
||||
line-height: 70px;
|
||||
background: url(/fas/static/images/head.png) 0 0 repeat-x;
|
||||
background: url(../images/head.png) 0 0 repeat-x;
|
||||
}
|
||||
|
||||
#head h1
|
||||
|
@ -34,7 +34,7 @@ a
|
|||
float: left;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
background: url(/fas/static/images/logo.png) 1ex 50% no-repeat;
|
||||
background: url(../images/logo.png) 1ex 50% no-repeat;
|
||||
}
|
||||
|
||||
#searchbox
|
||||
|
@ -65,7 +65,7 @@ a
|
|||
{
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
background: url(/fas/static/images/topnav.png) 0 0 repeat-x;
|
||||
background: url(../images/topnav.png) 0 0 repeat-x;
|
||||
font-size: 1.6ex;
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ a
|
|||
#topnav ul li
|
||||
{
|
||||
display: inline;
|
||||
background: url(/fas/static/images/topnav-separator.png) 0 50% no-repeat;
|
||||
background: url(../images/topnav-separator.png) 0 50% no-repeat;
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ a
|
|||
right: 0;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
background: url(/fas/static/images/infobar.png) 0 0 repeat-x;
|
||||
background: url(../images/infobar.png) 0 0 repeat-x;
|
||||
font-size: 1.6ex;
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ a
|
|||
#control ul li
|
||||
{
|
||||
display: inline;
|
||||
background: url(/fas/static/images/control-separator.png) 0 50% no-repeat;
|
||||
background: url(../images/control-separator.png) 0 50% no-repeat;
|
||||
}
|
||||
|
||||
#control a
|
||||
|
@ -150,14 +150,14 @@ a
|
|||
|
||||
#main
|
||||
{
|
||||
background: url(/fas/static/images/shadow.png) 0 0 repeat-x;
|
||||
background: url(../images/shadow.png) 0 0 repeat-x;
|
||||
}
|
||||
|
||||
#sidebar
|
||||
{
|
||||
width: 22ex;
|
||||
float: left;
|
||||
background: #335F9D url(/fas/static/images/sidebar.png) 0 0 repeat-x;
|
||||
background: #335F9D url(../images/sidebar.png) 0 0 repeat-x;
|
||||
border: 1px solid #112233;
|
||||
}
|
||||
|
||||
|
@ -246,25 +246,25 @@ a
|
|||
.account
|
||||
{
|
||||
padding-left: 30px;
|
||||
background: url(/fas/static/images/account.png) 0 68% no-repeat;
|
||||
background: url(../images/account.png) 0 68% no-repeat;
|
||||
}
|
||||
|
||||
.approved
|
||||
{
|
||||
padding-left: 20px;
|
||||
background: url(/fas/static/images/approved.png) 0 68% no-repeat;
|
||||
background: url(../images/approved.png) 0 68% no-repeat;
|
||||
}
|
||||
|
||||
.unapproved
|
||||
{
|
||||
padding-left: 20px;
|
||||
background: url(/fas/static/images/unapproved.png) 0 68% no-repeat;
|
||||
background: url(../images/unapproved.png) 0 68% no-repeat;
|
||||
}
|
||||
|
||||
.attn
|
||||
{
|
||||
padding-left: 20px;
|
||||
background: url(/fas/static/images/attn.png) 0 68% no-repeat;
|
||||
background: url(../images/attn.png) 0 68% no-repeat;
|
||||
}
|
||||
|
||||
.roleslist
|
||||
|
@ -302,7 +302,7 @@ a
|
|||
margin-top: 1ex;
|
||||
padding-top: 1ex;
|
||||
padding-left: 22px;
|
||||
background: url(/fas/static/images/arrow.png) 0 1.6ex no-repeat;
|
||||
background: url(../images/arrow.png) 0 1.6ex no-repeat;
|
||||
}
|
||||
|
||||
#rolespanel h4
|
||||
|
@ -332,13 +332,13 @@ a
|
|||
#rolespanel .tools li
|
||||
{
|
||||
padding-left: 22px;
|
||||
background: url(/fas/static/images/tools.png) 0 50% no-repeat;
|
||||
background: url(../images/tools.png) 0 50% no-repeat;
|
||||
}
|
||||
|
||||
#rolespanel .queue li
|
||||
{
|
||||
padding-left: 22px;
|
||||
background: url(/fas/static/images/queue.png) 0 50% no-repeat;
|
||||
background: url(../images/queue.png) 0 50% no-repeat;
|
||||
}
|
||||
|
||||
#rolespanel .queue strong
|
||||
|
@ -352,7 +352,7 @@ a
|
|||
clear: both;
|
||||
text-align: center;
|
||||
padding: 15px 0 2.5ex;
|
||||
background: url(/fas/static/images/footer-top.png) 0 0 repeat-x;
|
||||
background: url(../images/footer-top.png) 0 0 repeat-x;
|
||||
}
|
||||
|
||||
#footer .copy, #footer .disclaimer
|
||||
|
@ -364,7 +364,7 @@ a
|
|||
{
|
||||
padding-top: 3px;
|
||||
padding-bottom: 18px;
|
||||
background: #EEEEEE url(/fas/static/images/footer-bottom.png) 0 100% repeat-x;
|
||||
background: #EEEEEE url(../images/footer-bottom.png) 0 100% repeat-x;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
|
@ -389,7 +389,7 @@ a
|
|||
|
||||
.flash
|
||||
{
|
||||
background: #DEE6B1 url(/fas/static/images/success.png) 10px 50% no-repeat;
|
||||
background: #DEE6B1 url(../images/success.png) 10px 50% no-repeat;
|
||||
border: 1px solid #CCBBAA;
|
||||
padding: 1.5ex 15px 1.5ex 43px;
|
||||
margin: 1ex 0;
|
||||
|
@ -397,7 +397,7 @@ a
|
|||
|
||||
.help
|
||||
{
|
||||
background: #DEE6B1 url(/fas/static/images/help.png) 10px 50% no-repeat;
|
||||
background: #DEE6B1 url(../images/help.png) 10px 50% no-repeat;
|
||||
border: 1px solid #CCBBAA;
|
||||
padding: 1.5ex 15px 1.5ex 65px;
|
||||
margin: 1ex 0;
|
||||
|
|
|
@ -717,7 +717,7 @@ HelpBalloonOptions.prototype = {
|
|||
* to an existing element if you're using that as your anchoring icon.
|
||||
* @var {Object}
|
||||
*/
|
||||
icon: '/static/images/balloons/icon.gif',
|
||||
icon: '/accounts/static/images/balloons/icon.gif',
|
||||
/**
|
||||
* Alt text of the help icon
|
||||
* @var {String}
|
||||
|
@ -783,7 +783,7 @@ HelpBalloonOptions.prototype = {
|
|||
* Clossing button image path
|
||||
* @var {String}
|
||||
*/
|
||||
button: '/static/images/balloons/button.png',
|
||||
button: '/accounts/static/images/balloons/button.png',
|
||||
/**
|
||||
* Balloon image path prefix. There are 4 button images, numerically named, starting with 0.
|
||||
* 0, 1
|
||||
|
@ -791,7 +791,7 @@ HelpBalloonOptions.prototype = {
|
|||
* (the number indicates the corner opposite the anchor (the pointing direction)
|
||||
* @var {String}
|
||||
*/
|
||||
balloonPrefix: '/static/images/balloons/balloon-',
|
||||
balloonPrefix: '/accounts/static/images/balloons/balloon-',
|
||||
/**
|
||||
* The image filename suffix, including the file extension
|
||||
* @var {String}
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
</head>
|
||||
<body>
|
||||
<h2>${_('FAS - The Open Account System')}</h2>
|
||||
<p>${_('FAS is designed around an open architecture. Unlike the traditional account systems where a single admin or group of admins decide who gets to be in what group, FAS is completely designed to be self operating per team. Every group is given at least one administrator who can then approve other people in the group. Also, unlike traditional account systems. FAS allows people to apply for the groups they want to be in. This paridigm is interesting as it allows anyone to find out who is in what groups and contact them. This openness is brought over from the same philosophies that make Open Source popular.')}</p>
|
||||
<p>${_('''FAS is designed around an open architecture. Unlike the traditional account systems where a single admin or group of admins decide who gets to be in what group, FAS is completely designed to be self operating per team. Every group is given at least one administrator who can then approve other people in the group. Also, unlike traditional account systems. FAS allows people to apply for the groups they want to be in. This paridigm is interesting as it allows anyone to find out who is in what groups and contact them. This openness is brought over from the same philosophies that make Open Source popular.''')}</p>
|
||||
<h2>${_('Etiquette')}</h2>
|
||||
<p>${_('People shouldn't assume that by applying for a group that they're then in that group. Consider it like applying for another job. It often takes time. For best odds of success, learn about the group you're applying for and get to know someone in the group. Find someone with sponsor or admin access and ask them if they'd have time to mentor you. Plan on spending at least a few days learning about the group, doing a mundain task, participating on the mailing list. Sometimes this process can take weeks depending on the group. It's best to know you will get sponsored before you apply.')}
|
||||
<p>${_("People shouldn't assume that by applying for a group that they're then in that group. Consider it like applying for another job. It often takes time. For best odds of success, learn about the group you're applying for and get to know someone in the group. Find someone with sponsor or admin access and ask them if they'd have time to mentor you. Plan on spending at least a few days learning about the group, doing a mundain task, participating on the mailing list. Sometimes this process can take weeks depending on the group. It's best to know you will get sponsored before you apply.")}</p>
|
||||
<h2>${_('Users, Sponsors, Administrators')}</h2>
|
||||
<p>${_('Once you're in the group, you're in the group. Sponsorship and Administrators typically have special access in the group in questions. Some groups consider sponsorship level to be of a higher involvement, partial ownership of the group for example. But as far as the account system goes the disctinction is easy. Sponsors can approve new users and make people into sponsors. They cannot, however, downgrade or remove other sponsors. They also cannot change administrators in any way. Administrators can do anything to anyone in the group.')}
|
||||
<p>${_('''Once you're in the group, you're in the group. Sponsorship and Administrators typically have special access in the group in questions. Some groups consider sponsorship level to be of a higher involvement, partial ownership of the group for example. But as far as the account system goes the disctinction is easy. Sponsors can approve new users and make people into sponsors. They cannot, however, downgrade or remove other sponsors. They also cannot change administrators in any way. Administrators can do anything to anyone in the group.''')}</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<body>
|
||||
<h2>${_('Fedora Contributor License Agreement')}</h2>
|
||||
<p>
|
||||
<!-- TODO: Update to not mention click-through CLA (until it's ready) -->
|
||||
${Markup(_('There are two ways to sign the CLA. Most users will want to do a signed CLA as it will promote them to a full contributor in Fedora. The click-through CLA only grants partial access but may be preferred for those with special legal considerations. See: <a href="http://fedoraproject.org/wiki/Legal/CLAAcceptanceHierarchies">CLA Acceptance Hierarchies</a> for more information.'))}
|
||||
</p>
|
||||
<br/>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#for user in sorted(groups.keys())
|
||||
${user},${Person.byUserName(user).mail},${Person.byUserName(user).givenName},${groups[user].fedoraRoleType}
|
||||
#for role in sorted(group.approved_roles)
|
||||
${role.member.username},${role.member.emails['primary'].email},${role.member.human_name},${role.role_type}
|
||||
#end
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('Unapproved')}</span>
|
||||
</a>
|
||||
<a py:if="group not in person.memberships" href="${tg.url('/group/apply/%s/%s' % (group.name, person.username))}"><span>${_('Apply')}</span></a>
|
||||
<script py:if="group not in person.memberships" type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/group_apply'});</script>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -27,15 +27,15 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<label for="needs_sponsor">${_('Needs Sponsor:')}</label>
|
||||
<input type="checkbox" id="needs_sponsor" name="needs_sponsor" value="TRUE" checked="checked" />
|
||||
<input type="checkbox" id="needs_sponsor" name="needs_sponsor" value="1" checked="checked" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="user_can_remove">${_('Self Removal:')}</label>
|
||||
<input type="checkbox" id="user_can_remove" name="user_can_remove" value="TRUE" checked="checked" />
|
||||
<input type="checkbox" id="user_can_remove" name="user_can_remove" value=1"" checked="checked" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="prerequisite">${_('Must Belong To:')}</label>
|
||||
<input type="text" id="prerequisite" name="prerequisite" value="cla_done" />
|
||||
<input type="text" id="prerequisite" name="prerequisite" value="cla_sign" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="joinmsg">${_('Join Message:')}</label>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<a py:if="group in person.memberships" href="${tg.url('/group/remove/%s/%s' % (group.name, person.username))}">${_('Remove me')}</a>
|
||||
<script py:if="group in person.memberships" type="text/javascript">var hb7 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_remove')}'});</script>
|
||||
<h3>Group Details <a py:if="auth.canAdminGroup(person, group)" href="${tg.url('/group/edit/%s' % group.name)}">${_('(edit)')}</a></h3>
|
||||
<div class="userbox">
|
||||
<dl>
|
||||
|
@ -70,10 +71,11 @@
|
|||
</thead>
|
||||
<tr py:for="role in group.roles">
|
||||
<td><a href="${tg.url('/user/view/%s' % role.member.username)}">${role.member.username}</a></td>
|
||||
<td py:if='not(role.member.username == "None")'><a href="${tg.url('/user/view/%s' % role.member.username)}">${role.member.username}</a></td>
|
||||
<td py:if='role.member.username == "None"'>${_('None')}</td>
|
||||
<td py:if='role.sponsor'><a href="${tg.url('/user/view/%s' % role.sponsor.username)}">${role.sponsor.username}</a></td>
|
||||
<td py:if='not role.sponsor'>${_('None')}</td>
|
||||
<td>${role.creation.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}</td>
|
||||
<td>${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}</td>
|
||||
<td py:if='role.approval'>${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}</td>
|
||||
<td py:if='not role.approval'>${_('None')}</td>
|
||||
<td>${role.role_status}</td>
|
||||
<td>${role.role_type}</td>
|
||||
<!-- This section includes all action items -->
|
||||
|
@ -81,16 +83,21 @@
|
|||
<ul class="actions">
|
||||
<li py:if="group in role.member.unapproved_memberships">
|
||||
<a py:if="group.needs_sponsor" href="${tg.url('/group/sponsor/%s/%s' % (group.name, role.member.username))}">${_('Sponsor')}</a>
|
||||
<script py:if="group.needs_sponsor" type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_sponsor')}'});</script>
|
||||
<a py:if="not group.needs_sponsor" href="${tg.url('/group/sponsor/%s/%s' % (group.name, role.member.username))}">${_('Approve')}</a>
|
||||
<script py:if="not group.needs_sponsor" type="text/javascript">var hb2 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_approve')}'});</script>
|
||||
</li>
|
||||
<li py:if="auth.canRemoveUser(person, group, role.member)">
|
||||
<a href="${tg.url('/group/remove/%s/%s' % (group.name, role.member.username))}">${_('Remove')}</a>
|
||||
<script type="text/javascript">var hb3 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_remove')}'});</script>
|
||||
</li>
|
||||
<li py:if="auth.canUpgradeUser(person, group, role.member)">
|
||||
<a href="${tg.url('/group/upgrade/%s/%s' % (group.name, role.member.username))}">${_('Upgrade')}</a>
|
||||
<script type="text/javascript">var hb4 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_upgrade')}'});</script>
|
||||
</li>
|
||||
<li py:if="auth.canDowngradeUser(person, group, role.member)">
|
||||
<a href="${tg.url('/group/downgrade/%s/%s' % (group.name, role.member.username))}">${_('Downgrade')}</a>
|
||||
<script type="text/javascript">var hb5 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/group_downgrade')}'});</script>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
|
|
@ -7,5 +7,25 @@
|
|||
<title>${_('Fedora Accounts System')}</title>
|
||||
</head>
|
||||
<body>
|
||||
<?python from fas import auth ?>
|
||||
<h2>Todo queue:</h2>
|
||||
<py:for each="group in sorted(person.memberships)">
|
||||
<dl>
|
||||
<py:if test="auth.canSponsorGroup(person, group) and group.unapproved_roles">
|
||||
<dd>
|
||||
<ul class="queue">
|
||||
<li py:for="role in group.unapproved_roles[:5]">
|
||||
${Markup(_('<strong>%(user)s</strong> requests approval to join <a href="group/view/%(group)s">%(group)s</a>.') % {'user': role.member.username, 'group': group.name, 'group': group.name})}
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</py:if>
|
||||
</dl>
|
||||
</py:for>
|
||||
<ul class="queue">
|
||||
<li py:if="cla == None">
|
||||
${_('CLA Not Signed. To become a full Fedora Contributor please ')}<a href="${tg.url('/cla/')}">${_('sign the CLA')}</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
<div id="control">
|
||||
<ul>
|
||||
<li><a href="${tg.url('/about')}">about</a></li>
|
||||
<li><a href="${tg.url('/about')}">About</a></li>
|
||||
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/user/view/%s' % tg.identity.user.username)}">${_('My Account')}</a></li>
|
||||
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/logout')}">${_('Log Out')}</a></li>
|
||||
<li py:if="tg.identity.anonymous"><a href="${tg.url('/login')}">${_('Log In')}</a></li>
|
||||
|
@ -56,13 +56,14 @@
|
|||
<div id="main">
|
||||
<div id="sidebar">
|
||||
<ul>
|
||||
<li class="first"><a href="${tg.url('/group/list')}">${_('Group List')}</a></li>
|
||||
<li class="first"><a href="${tg.url('/home')}">${_('Home')}</a></li>
|
||||
<div py:if="not tg.identity.anonymous and 'accounts' in tg.identity.groups" py:strip=''>
|
||||
<!-- TODO: Make these use auth.py -->
|
||||
<li><a href="${tg.url('/user/list')}">${_('User List')}</a></li>
|
||||
<li><a href="${tg.url('/group/new')}">${_('New Group')}</a></li>
|
||||
<li><a href="${tg.url('/user/list')}">${_('User List')}</a></li>
|
||||
</div>
|
||||
<li><a href="${tg.url('/group/list/A*')}">${_('Apply For a new Group')}</a></li>
|
||||
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/group/list')}">${_('Group List')}</a></li>
|
||||
<li py:if="not tg.identity.anonymous"><a href="${tg.url('/group/list/A*')}">${_('Apply For a new Group')}</a></li>
|
||||
<li><a href="http://fedoraproject.org/wiki/FWN/LatestIssue">${_('News')}</a></li>
|
||||
</ul>
|
||||
<div py:if="tg.identity.anonymous" id="language">
|
||||
|
@ -74,14 +75,34 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div py:if="tg_flash" class="flash">
|
||||
<div style="
|
||||
background: #FF7777;
|
||||
color: #333333;
|
||||
border: 1px solid #555555;
|
||||
padding: 1ex 1ex 0.5ex;
|
||||
float: right;
|
||||
width: 57ex;
|
||||
margin: 1ex 1ex 1ex 2ex;
|
||||
">
|
||||
<div style="font-size: 3ex;"><strong>Warning:</strong> This is a test instance!</div>
|
||||
<ul style="
|
||||
font-size: 2ex;
|
||||
list-style: square;
|
||||
padding-left: 3ex;
|
||||
">
|
||||
<li>Avoid entering private info here.</li>
|
||||
<li>The database may be wiped/rebuilt periodically.</li>
|
||||
<li>Feel free to file bugs, enhancements, etc. at <a href="https://fedorahosted.org/fas2/">https://fedorahosted.org/fas2/</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div py:if="tg_flash" class="flash" style="margin-right: 62ex">
|
||||
${tg_flash}
|
||||
</div>
|
||||
<div py:replace="select('*|text()')" />
|
||||
</div>
|
||||
<div id="footer">
|
||||
<ul id="footlinks">
|
||||
<li class="first"><a href="/">${_('About')}</a></li>
|
||||
<li class="first"><a href="${tg.url('/about')}">${_('About')}</a></li>
|
||||
<li><a href="http://fedoraproject.org/wiki/Communicate">${_('Contact Us')}</a></li>
|
||||
<li><a href="http://fedoraproject.org/wiki/Legal">${_('Legal & Privacy')}</a></li>
|
||||
<!--<li><a href="/">Site Map</a></li>-->
|
||||
|
|
|
@ -12,13 +12,13 @@
|
|||
<div class="field">
|
||||
<label for="human_name">${_('Human Name')}:</label>
|
||||
<input type="text" id="human_name" name="human_name" value="${target.human_name}" />
|
||||
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/user_human_name'});</script>
|
||||
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_human_name')}'});</script>
|
||||
</div>
|
||||
<!--Need to figure out what the interface should be for emails. -->
|
||||
<div class="field">
|
||||
<label for="mail">${_('Email')}:</label>
|
||||
<input type="text" id="email" name="email" value="${target.emails['primary'].email}" />
|
||||
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '/fas/help/get_help/user_primary_email'});</script>
|
||||
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_primary_email')}'});</script>
|
||||
</div>
|
||||
<!-- <div class="field">
|
||||
<label for="fedoraPersonBugzillaMail">${_('Bugzilla Email')}:</label>
|
||||
|
@ -27,22 +27,22 @@
|
|||
<div class="field">
|
||||
<label for="ircnick">${_('IRC Nick')}:</label>
|
||||
<input type="text" id="ircnick" name="ircnick" value="${target.ircnick}" />
|
||||
<script type="text/javascript">var hb3 = new HelpBalloon({dataURL: '/fas/help/get_help/user_ircnick'});</script>
|
||||
<script type="text/javascript">var hb3 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_ircnick')}'});</script>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="gpg_keyid">${_('PGP Key')}:</label>
|
||||
<input type="text" id="gpg_keyid" name="gpg_keyid" value="${target.gpg_keyid}" />
|
||||
<script type="text/javascript">var hb4 = new HelpBalloon({dataURL: '/fas/help/get_help/user_gpg_keyid'});</script>
|
||||
<script type="text/javascript">var hb4 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_gpg_keyid')}'});</script>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="telephone">${_('Telephone Number')}:</label>
|
||||
<input type="text" id="telephone" name="telephone" value="${target.telephone}" />
|
||||
<script type="text/javascript">var hb5 = new HelpBalloon({dataURL: '/fas/help/get_help/user_telephone'});</script>
|
||||
<script type="text/javascript">var hb5 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_telephone')}'});</script>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="postal_address">${_('Postal Address')}:</label>
|
||||
<textarea id="postal_address" name="postal_address">${target.postal_address}</textarea>
|
||||
<script type="text/javascript">var hb6 = new HelpBalloon({dataURL: '/fas/help/get_help/user_postal_address'});</script>
|
||||
<script type="text/javascript">var hb6 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_postal_address')}'});</script>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="timezone">${_('Time Zone')}:</label>
|
||||
|
@ -52,22 +52,23 @@
|
|||
?>
|
||||
<option py:for="tz in common_timezones" value="${tz}" py:attrs="{'selected': target.timezone == tz and 'selected' or None}">${tz}</option>
|
||||
</select>
|
||||
<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '/fas/help/get_help/user_timezone'});</script>
|
||||
<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_timezone')}'});</script>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="locale">${_('Locale')}:</label>
|
||||
<input type="text" id="locale" name="locale" value="${target.locale}" />
|
||||
<script type="text/javascript">var hb8 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_locale')}'});</script>
|
||||
<!--
|
||||
<select id="locale" name="locale">
|
||||
option py:for="locale in available_locales" value="${locale}" py:attrs="{'selected': target.locale == locale and 'selected' or None}">${locale}</option>
|
||||
</select>
|
||||
-->
|
||||
<!--<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '/fas/help/get_help/locale'});</script>-->
|
||||
<!--<script type="text/javascript">var hb7 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/locale')}'});</script>-->
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="comments ">${_('Comments')}:</label>
|
||||
<textarea id="comments" name="comments">${target.comments}</textarea>
|
||||
<script type="text/javascript">var hb8 = new HelpBalloon({dataURL: '/fas/help/get_help/user_comments'});</script>
|
||||
<script type="text/javascript">var hb8 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_comments')}'});</script>
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="submit" value="${_('Save!')}" />
|
||||
|
|
|
@ -19,18 +19,21 @@
|
|||
<!--<dt>${_('Bugzilla Email:')}</dt><dd>${person.username}</dd>-->
|
||||
<dt>${_('IRC Nick:')}</dt><dd>${person.ircnick} </dd>
|
||||
<dt>${_('PGP Key:')}</dt><dd>${person.gpg_keyid} </dd>
|
||||
<dt>${_('Telephone Number:')}</dt><dd>${person.telephone} </dd>
|
||||
<dt>${_('Postal Address:')}</dt><dd>${person.postal_address} </dd>
|
||||
<py:if test="personal"><dt>${_('Telephone Number:')}</dt><dd>${person.telephone} </dd></py:if>
|
||||
<py:if test="personal"><dt>${_('Postal Address:')}</dt><dd>${person.postal_address} </dd></py:if>
|
||||
<dt>${_('Comments:')}</dt><dd>${person.comments} </dd>
|
||||
<dt>${_('Password:')}</dt><dd><span class="approved">${_('Valid')}</span> <a href="${tg.url('/user/changepass')}" py:if="personal">(change)</a></dd>
|
||||
<dt>${_('Account Status:')}</dt><dd><span class="approved">${_('Valid')}</span>
|
||||
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '/fas/help/get_help/user_account_status'});</script></dd>
|
||||
<!-- cla = {None, 'signed', 'clicked'} -->
|
||||
<py:if test="personal"><dt>${_('Password:')}</dt><dd><span class="approved">${_('Valid')}</span> <a href="${tg.url('/user/changepass')}">(change)</a></dd></py:if>
|
||||
<dt>${_('Account Status:')}</dt><dd>
|
||||
<span py:if="person.status == 'active'" class="approved">${_('Active')}</span>
|
||||
<span py:if="person.status == 'vacation'" class="approved">${_('Vacation')}</span>
|
||||
<span py:if="person.status == 'inactive'" class="unapproved">${_('Inactive')}</span>
|
||||
<span py:if="person.status == 'pinged'" class="approved">${_('Pinged')}</span>
|
||||
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_account_status')}'});</script></dd>
|
||||
<dt>${_('CLA:')}</dt><dd>
|
||||
<span py:if="cla == 'signed'" class="approved">${_('Signed CLA')}</span>
|
||||
<span py:if="cla == 'clicked'" class="approved">${_('Click-through CLA')}<py:if test="personal">(<a href="${tg.url('/cla/')}">${_('GPG Sign it!')}</a></py:if>)</span>
|
||||
<span py:if="not cla" class="unapproved">${_('Not Done')}<py:if test="personal"> (<a href="${tg.url('/cla/')}">${_('Sign it!')}</a>)</py:if></span>
|
||||
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '/fas/help/get_help/user_cla'});</script></dd>
|
||||
<script type="text/javascript">var hb2 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_cla')}'});</script></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<h3 py:if="personal">${_('Your Roles')}</h3>
|
||||
|
@ -54,7 +57,7 @@
|
|||
<dt>${_('Status:')}</dt>
|
||||
<dd>
|
||||
<span class="approved" py:if="group in person.approved_memberships">${_('Approved')}</span>
|
||||
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('Unapproved')}</span>
|
||||
<span class="unapproved" py:if="group in person.unapproved_memberships">${_('None')}</span>
|
||||
</dd>
|
||||
<dt>${_('Tools:')}</dt>
|
||||
<dd>
|
||||
|
|
|
@ -65,7 +65,10 @@ class ValidUsername(validators.FancyValidator):
|
|||
|
||||
class UserSave(validators.Schema):
|
||||
targetname = KnownUser
|
||||
human_name = validators.String(not_empty=True, max=42)
|
||||
human_name = validators.All(
|
||||
validators.String(not_empty=True, max=42),
|
||||
validators.Regex(regex='^[^\n:<>]+$'),
|
||||
)
|
||||
#mail = validators.All(
|
||||
# validators.Email(not_empty=True, strip=True, max=128),
|
||||
# NonFedoraEmail(not_empty=True, strip=True, max=128),
|
||||
|
@ -81,7 +84,10 @@ class UserCreate(validators.Schema):
|
|||
validators.String(max=32, min=3),
|
||||
validators.Regex(regex='^[a-z][a-z0-9]+$'),
|
||||
)
|
||||
human_name = validators.String(not_empty=True, max=42)
|
||||
human_name = validators.All(
|
||||
validators.String(not_empty=True, max=42),
|
||||
validators.Regex(regex='^[^\n:<>]+$'),
|
||||
)
|
||||
email = validators.All(
|
||||
validators.Email(not_empty=True, strip=True),
|
||||
NonFedoraEmail(not_empty=True, strip=True),
|
||||
|
@ -211,7 +217,7 @@ class User(controllers.Controller):
|
|||
target = People.by_username(target)
|
||||
|
||||
if not canEditUser(person, target):
|
||||
turbogears.flash(_("You do not have permission to edit '%s'" % target.username))
|
||||
turbogears.flash(_("You do not have permission to edit '%s'") % target.username)
|
||||
turbogears.redirect('/user/edit/%s', target.username)
|
||||
return dict()
|
||||
try:
|
||||
|
@ -226,7 +232,7 @@ class User(controllers.Controller):
|
|||
target.locale = locale
|
||||
target.timezone = timezone
|
||||
except TypeError:
|
||||
turbogears.flash(_('Your account details could not be saved: %s' % e))
|
||||
turbogears.flash(_('Your account details could not be saved: %s') % e)
|
||||
else:
|
||||
turbogears.flash(_('Your account details have been saved.'))
|
||||
turbogears.redirect("/user/view/%s" % target.username)
|
||||
|
@ -267,6 +273,7 @@ class User(controllers.Controller):
|
|||
person.human_name = human_name
|
||||
person.telephone = telephone
|
||||
person.password = '*'
|
||||
person.status = 'active'
|
||||
person.emails['primary'] = PersonEmails(email=email, purpose='primary')
|
||||
newpass = generate_password()
|
||||
message = turbomail.Message(config.get('accounts_mail'), person.emails['primary'].email, _('Welcome to the Fedora Project!'))
|
||||
|
@ -378,10 +385,16 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
|
|||
# CLA one), think of how to make sure this doesn't get
|
||||
# full of random keys (keep a clean Fedora keyring)
|
||||
# TODO: MIME stuff?
|
||||
try:
|
||||
subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', person.gpg_keyid])
|
||||
except subprocess.CalledProcessError:
|
||||
keyid = re.sub('\s', '', person.gpg_keyid)
|
||||
ret = subprocess.call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
|
||||
if ret != 0:
|
||||
turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
|
||||
turbogears.redirect('/cla/view/sign')
|
||||
return dict()
|
||||
#try:
|
||||
# subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid])
|
||||
#except subprocess.CalledProcessError:
|
||||
# turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net"))
|
||||
else:
|
||||
try:
|
||||
plaintext = StringIO.StringIO(mail)
|
||||
|
@ -390,7 +403,7 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
|
|||
ctx.armor = True
|
||||
signer = ctx.get_key(re.sub('\s', '', config.get('gpg_fingerprint')))
|
||||
ctx.signers = [signer]
|
||||
recipient = ctx.get_key(re.sub('\s', '', person.gpg_keyid))
|
||||
recipient = ctx.get_key(keyid)
|
||||
def passphrase_cb(uid_hint, passphrase_info, prev_was_bad, fd):
|
||||
os.write(fd, '%s\n' % config.get('gpg_passphrase'))
|
||||
ctx.passphrase_cb = passphrase_cb
|
||||
|
@ -406,7 +419,7 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
|
|||
message.plain = mail;
|
||||
turbomail.enqueue(message)
|
||||
try:
|
||||
person.password = newpass['pass']
|
||||
person.password = newpass['hash']
|
||||
turbogears.flash(_('Your new password has been emailed to you.'))
|
||||
except:
|
||||
turbogears.flash(_('Your password could not be reset.'))
|
||||
|
@ -419,7 +432,7 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
|
|||
def gencert(self):
|
||||
from fas.openssl_fas import *
|
||||
username = turbogears.identity.current.user_name
|
||||
person = Person.by_username(username)
|
||||
person = People.by_username(username)
|
||||
|
||||
person.certificate_serial = person.certificate_serial + 1
|
||||
|
||||
|
@ -438,8 +451,8 @@ Please go to https://admin.fedoraproject.org/fas/ to change it.
|
|||
L=config.get('openssl_l'),
|
||||
O=config.get('openssl_o'),
|
||||
OU=config.get('openssl_ou'),
|
||||
CN=user.cn,
|
||||
emailAddress=person.mail,
|
||||
CN=person.username,
|
||||
emailAddress=person.emails['primary'].email,
|
||||
)
|
||||
|
||||
cert = createCertificate(req, (cacert, cakey), person.certificate_serial, (0, expire), digest='md5')
|
||||
|
|
|
@ -56,7 +56,7 @@ CREATE TABLE people (
|
|||
internal_comments TEXT,
|
||||
ircnick TEXT,
|
||||
last_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
status_change TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
locale TEXT not null DEFAULT 'C',
|
||||
timezone TEXT null DEFAULT 'UTC',
|
||||
|
|
368
fas/po/fas.pot
368
fas/po/fas.pot
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2008-03-03 00:46-0500\n"
|
||||
"POT-Creation-Date: 2008-03-04 22:01-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,32 +17,36 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 0.9.1\n"
|
||||
|
||||
#: client/fasClient.py:42
|
||||
#: client/fasClient.py:41
|
||||
msgid "Download and sync most recent content"
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:47
|
||||
#: client/fasClient.py:46
|
||||
#, python-format
|
||||
msgid "Specify config file (default \"%default\")"
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:51
|
||||
msgid "Do not sync group information"
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:52
|
||||
#: client/fasClient.py:56
|
||||
msgid "Do not sync passwd information"
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:57
|
||||
#: client/fasClient.py:61
|
||||
msgid "Do not sync shadow information"
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:62
|
||||
#, python-format
|
||||
msgid "Specify URL of fas server (default \"%default\")"
|
||||
#: client/fasClient.py:66
|
||||
msgid "Specify URL of fas server."
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:67
|
||||
#: client/fasClient.py:71
|
||||
msgid "Enable FAS synced shell accounts"
|
||||
msgstr ""
|
||||
|
||||
#: client/fasClient.py:72
|
||||
#: client/fasClient.py:76
|
||||
msgid "Disable FAS synced shell accounts"
|
||||
msgstr ""
|
||||
|
||||
|
@ -59,92 +63,98 @@ msgstr ""
|
|||
msgid "%s membership required before application to this group is allowed"
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:52 fas/cla.py:178
|
||||
#: fas/cla.py:52
|
||||
msgid ""
|
||||
"You have already signed the CLA, so it is unnecessary to complete the "
|
||||
"Click-through CLA."
|
||||
"To sign the CLA we must have your telephone number, postal address and "
|
||||
"gpg key id. Please ensure they have been filled out"
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:56 fas/cla.py:182
|
||||
msgid "You have already completed the Click-through CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:61 fas/cla.py:87
|
||||
#: fas/cla.py:69 fas/cla.py:95
|
||||
msgid "You have already signed the CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:100 fas/user.py:380
|
||||
#: fas/cla.py:108 fas/user.py:390
|
||||
msgid "Your key could not be retrieved from subkeys.pgp.net"
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:107
|
||||
#: fas/cla.py:121
|
||||
#, python-format
|
||||
msgid "Your signature could not be verified: '%s'."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:117
|
||||
#: fas/cla.py:131
|
||||
msgid ""
|
||||
"Your signature's fingerprint did not match the fingerprint registered in "
|
||||
"FAS."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:126
|
||||
#: fas/cla.py:140
|
||||
msgid "Your key did not match your email."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:131
|
||||
#: fas/cla.py:145
|
||||
msgid "len(sigs) == 0"
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:137
|
||||
#: fas/cla.py:151
|
||||
msgid "The GPG-signed part of the message did not contain a signed CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:142
|
||||
#: fas/cla.py:156
|
||||
msgid "The text \"I agree\" was not found in the CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:156 fas/cla.py:194
|
||||
#: fas/cla.py:170 fas/cla.py:210
|
||||
#, python-format
|
||||
msgid "You could not be added to the '%s' group."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:165
|
||||
#: fas/cla.py:180
|
||||
#, python-format
|
||||
msgid "You have successfully signed the CLA. You are now in the '%s' group."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:194
|
||||
msgid ""
|
||||
"You have already signed the CLA, so it is unnecessary to complete the "
|
||||
"Click-through CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:198
|
||||
msgid "You have already completed the Click-through CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:214
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have successfully agreed to the click-through CLA. You are now in "
|
||||
"the '%s' group."
|
||||
msgstr ""
|
||||
|
||||
#: fas/cla.py:202
|
||||
#: fas/cla.py:218
|
||||
msgid "You have not agreed to the click-through CLA."
|
||||
msgstr ""
|
||||
|
||||
#: fas/controllers.py:84
|
||||
#: fas/controllers.py:98
|
||||
#, python-format
|
||||
msgid "Welcome, %s"
|
||||
msgstr ""
|
||||
|
||||
#: fas/controllers.py:99
|
||||
#: fas/controllers.py:113
|
||||
msgid ""
|
||||
"The credentials you supplied were not correct or did not grant access to "
|
||||
"this resource."
|
||||
msgstr ""
|
||||
|
||||
#: fas/controllers.py:102
|
||||
#: fas/controllers.py:116
|
||||
msgid "You must provide your credentials before accessing this resource."
|
||||
msgstr ""
|
||||
|
||||
#: fas/controllers.py:105
|
||||
#: fas/controllers.py:119
|
||||
msgid "Please log in."
|
||||
msgstr ""
|
||||
|
||||
#: fas/controllers.py:116
|
||||
#: fas/controllers.py:131
|
||||
msgid "You have successfully logged out."
|
||||
msgstr ""
|
||||
|
||||
|
@ -158,7 +168,7 @@ msgstr ""
|
|||
msgid "The group '%s' already exists."
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:129 fas/group.py:467
|
||||
#: fas/group.py:129 fas/group.py:469
|
||||
#, python-format
|
||||
msgid "You cannot view '%s'"
|
||||
msgstr ""
|
||||
|
@ -207,7 +217,7 @@ msgstr ""
|
|||
msgid "%(user)s has already applied to %(group)s!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:293
|
||||
#: fas/group.py:295
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -218,22 +228,22 @@ msgid ""
|
|||
"Please go to %(url)s to take action. \n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:300
|
||||
#: fas/group.py:302
|
||||
#, python-format
|
||||
msgid "%(user)s has applied to %(group)s!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:317
|
||||
#: fas/group.py:319
|
||||
#, python-format
|
||||
msgid "You cannot sponsor '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:324
|
||||
#: fas/group.py:326
|
||||
#, python-format
|
||||
msgid "'%s' could not be sponsored!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:329
|
||||
#: fas/group.py:331
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -244,22 +254,22 @@ msgid ""
|
|||
"%(joinmsg)s\n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:337
|
||||
#: fas/group.py:339
|
||||
#, python-format
|
||||
msgid "'%s' has been sponsored!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:354
|
||||
#: fas/group.py:356
|
||||
#, python-format
|
||||
msgid "You cannot remove '%s'."
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:361
|
||||
#: fas/group.py:363
|
||||
#, python-format
|
||||
msgid "%(name)s could not be removed from %(group)s!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:367
|
||||
#: fas/group.py:369
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -269,22 +279,22 @@ msgid ""
|
|||
"aliases within an hour.\n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:374
|
||||
#: fas/group.py:376
|
||||
#, python-format
|
||||
msgid "%(name)s has been removed from %(group)s!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:391
|
||||
#: fas/group.py:393
|
||||
#, python-format
|
||||
msgid "You cannot upgrade '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:401
|
||||
#: fas/group.py:403
|
||||
#, python-format
|
||||
msgid "%(name)s could not be upgraded!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:409
|
||||
#: fas/group.py:411
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -294,22 +304,22 @@ msgid ""
|
|||
"into the e-mail aliases within an hour.\n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:416
|
||||
#: fas/group.py:418
|
||||
#, python-format
|
||||
msgid "%s has been upgraded!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:432
|
||||
#: fas/group.py:434
|
||||
#, python-format
|
||||
msgid "You cannot downgrade '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:439
|
||||
#: fas/group.py:441
|
||||
#, python-format
|
||||
msgid "%(username)s could not be downgraded!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:446
|
||||
#: fas/group.py:448
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -319,16 +329,16 @@ msgid ""
|
|||
"into the e-mail aliases within an hour.\n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:453
|
||||
#: fas/group.py:455
|
||||
#, python-format
|
||||
msgid "%s has been downgraded!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:495
|
||||
#: fas/group.py:497
|
||||
msgid "Come join The Fedora Project!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:496
|
||||
#: fas/group.py:498
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -349,12 +359,12 @@ msgid ""
|
|||
"Fedora and FOSS are changing the world -- come be a part of it!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:513
|
||||
#: fas/group.py:515
|
||||
#, python-format
|
||||
msgid "Message sent to: %s"
|
||||
msgstr ""
|
||||
|
||||
#: fas/group.py:516
|
||||
#: fas/group.py:518
|
||||
#, python-format
|
||||
msgid "You are not in the '%s' group."
|
||||
msgstr ""
|
||||
|
@ -392,39 +402,39 @@ msgstr ""
|
|||
msgid "'%s' is an illegal username."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:196
|
||||
#: fas/user.py:205
|
||||
#, python-format
|
||||
msgid "You cannot edit %s"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:211
|
||||
#: fas/user.py:220
|
||||
#, python-format
|
||||
msgid "You do not have permission to edit '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:226
|
||||
#: fas/user.py:235
|
||||
#, python-format
|
||||
msgid "Your account details could not be saved: %s"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:228
|
||||
#: fas/user.py:237
|
||||
msgid "Your account details have been saved."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:242
|
||||
#: fas/user.py:251
|
||||
#, python-format
|
||||
msgid "No users found matching '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:248
|
||||
#: fas/user.py:257
|
||||
msgid "No need to sign up, you have an account!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:269
|
||||
#: fas/user.py:278
|
||||
msgid "Welcome to the Fedora Project!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:270
|
||||
#: fas/user.py:279
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -466,42 +476,42 @@ msgid ""
|
|||
"forward to working with you!\n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:310
|
||||
#: fas/user.py:319
|
||||
msgid ""
|
||||
"Your password has been emailed to you. Please log in with it and change "
|
||||
"your password"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:313
|
||||
#: fas/user.py:322
|
||||
#, python-format
|
||||
msgid "The username '%s' already Exists. Please choose a different username."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:338
|
||||
#: fas/user.py:348
|
||||
msgid "Your password has been changed."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:341
|
||||
#: fas/user.py:351
|
||||
msgid "Your password could not be changed."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:347
|
||||
#: fas/user.py:357
|
||||
msgid "You are already logged in!"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:356
|
||||
#: fas/user.py:366
|
||||
msgid "You are already logged in."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:362
|
||||
#: fas/user.py:372
|
||||
msgid "username + email combo unknown."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:365
|
||||
#: fas/user.py:375
|
||||
msgid "Fedora Project Password Reset"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:366
|
||||
#: fas/user.py:376
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
|
@ -511,20 +521,74 @@ msgid ""
|
|||
"Please go to https://admin.fedoraproject.org/fas/ to change it.\n"
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:399
|
||||
#: fas/user.py:415
|
||||
msgid ""
|
||||
"Your password reset email could not be encrypted. Your password has not "
|
||||
"been changed."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:406
|
||||
#: fas/user.py:422
|
||||
msgid "Your new password has been emailed to you."
|
||||
msgstr ""
|
||||
|
||||
#: fas/user.py:408
|
||||
#: fas/user.py:424
|
||||
msgid "Your password could not be reset."
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:7
|
||||
msgid "About FAS"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:10
|
||||
msgid "FAS - The Open Account System"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:11
|
||||
msgid ""
|
||||
"FAS is designed around an open architecture. Unlike the traditional "
|
||||
"account systems where a single admin or group of admins decide who gets "
|
||||
"to be in what group, FAS is completely designed to be self operating per "
|
||||
"team. Every group is given at least one administrator who can then "
|
||||
"approve other people in the group. Also, unlike traditional account "
|
||||
"systems. FAS allows people to apply for the groups they want to be in. "
|
||||
"This paridigm is interesting as it allows anyone to find out who is in "
|
||||
"what groups and contact them. This openness is brought over from the "
|
||||
"same philosophies that make Open Source popular."
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:12
|
||||
msgid "Etiquette"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:13
|
||||
msgid ""
|
||||
"People shouldn't assume that by applying for a group that they're then in"
|
||||
" that group. Consider it like applying for another job. It often takes "
|
||||
"time. For best odds of success, learn about the group you're applying "
|
||||
"for and get to know someone in the group. Find someone with sponsor or "
|
||||
"admin access and ask them if they'd have time to mentor you. Plan on "
|
||||
"spending at least a few days learning about the group, doing a mundain "
|
||||
"task, participating on the mailing list. Sometimes this process can take"
|
||||
" weeks depending on the group. It's best to know you will get sponsored "
|
||||
"before you apply."
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:14
|
||||
msgid "Users, Sponsors, Administrators"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/about.html:15
|
||||
msgid ""
|
||||
"Once you're in the group, you're in the group. Sponsorship and "
|
||||
"Administrators typically have special access in the group in questions. "
|
||||
"Some groups consider sponsorship level to be of a higher involvement, "
|
||||
"partial ownership of the group for example. But as far as the account "
|
||||
"system goes the disctinction is easy. Sponsors can approve new users and"
|
||||
" make people into sponsors. They cannot, however, downgrade or remove "
|
||||
"other sponsors. They also cannot change administrators in any way. "
|
||||
"Administrators can do anything to anyone in the group."
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/error.html:7 fas/templates/home.html:7
|
||||
#: fas/templates/cla/click.html:7 fas/templates/cla/index.html:7
|
||||
#: fas/templates/cla/view.html:7 fas/templates/openid/about.html:7
|
||||
|
@ -604,66 +668,66 @@ msgstr ""
|
|||
msgid "Logged in:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:49
|
||||
#: fas/templates/master.html:50
|
||||
msgid "My Account"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:50 fas/templates/master.html:87
|
||||
#: fas/templates/master.html:51 fas/templates/master.html:88
|
||||
msgid "Log Out"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:51 fas/templates/welcome.html:21
|
||||
#: fas/templates/master.html:52 fas/templates/welcome.html:21
|
||||
msgid "Log In"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:58
|
||||
msgid "Group List"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:61
|
||||
msgid "User List"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:62
|
||||
msgid "New Group"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:64
|
||||
msgid "Apply For a new Group"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:65
|
||||
#: fas/templates/master.html:59
|
||||
msgid "News"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:69
|
||||
#: fas/templates/master.html:62
|
||||
msgid "User List"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:63
|
||||
msgid "New Group"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:65
|
||||
msgid "Apply For a new Group"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:66
|
||||
msgid "Group List"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:70
|
||||
msgid "Locale:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:71
|
||||
#: fas/templates/master.html:72
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:83
|
||||
#: fas/templates/master.html:84
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:84
|
||||
#: fas/templates/master.html:85
|
||||
msgid "Contact Us"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:85
|
||||
#: fas/templates/master.html:86
|
||||
msgid "Legal & Privacy"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:90
|
||||
#: fas/templates/master.html:91
|
||||
msgid ""
|
||||
"Copyright © 2007 Red Hat, Inc. and others. All Rights Reserved. Please "
|
||||
"send any comments or corrections to the <a "
|
||||
"href=\"mailto:webmaster@fedoraproject.org\">websites team</a>."
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/master.html:93
|
||||
#: fas/templates/master.html:94
|
||||
msgid ""
|
||||
"The Fedora Project is maintained and driven by the community and "
|
||||
"sponsored by Red Hat. This is a community maintained site. Red Hat is "
|
||||
|
@ -719,7 +783,7 @@ msgstr ""
|
|||
msgid "Fedora Contributor License Agreement"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/cla/index.html:12
|
||||
#: fas/templates/cla/index.html:13
|
||||
msgid ""
|
||||
"There are two ways to sign the CLA. Most users will want to do a signed "
|
||||
"CLA as it will promote them to a full contributor in Fedora. The click-"
|
||||
|
@ -729,17 +793,11 @@ msgid ""
|
|||
" Acceptance Hierarchies</a> for more information."
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/cla/index.html:15 fas/templates/user/list.html:42
|
||||
#: fas/templates/user/view.html:30
|
||||
msgid "Signed CLA"
|
||||
#: fas/templates/cla/index.html:18
|
||||
msgid "Sign Contributor License Agreement (CLA)"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/cla/index.html:16 fas/templates/user/list.html:43
|
||||
#: fas/templates/user/view.html:31
|
||||
msgid "Click-through CLA"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/cla/index.html:19
|
||||
#: fas/templates/cla/index.html:23
|
||||
#, python-format
|
||||
msgid "You have already sucessfully signed the <a href=\"%s\">CLA</a>."
|
||||
msgstr ""
|
||||
|
@ -783,12 +841,12 @@ msgid "Group Owner:"
|
|||
msgstr ""
|
||||
|
||||
#: fas/templates/group/edit.html:25 fas/templates/group/new.html:29
|
||||
#: fas/templates/group/view.html:38
|
||||
#: fas/templates/group/view.html:39
|
||||
msgid "Needs Sponsor:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/edit.html:30 fas/templates/group/new.html:33
|
||||
#: fas/templates/group/view.html:42
|
||||
#: fas/templates/group/view.html:43
|
||||
msgid "Self Removal:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -800,7 +858,7 @@ msgstr ""
|
|||
msgid "Group Join Message:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/edit.html:44 fas/templates/user/edit.html:73
|
||||
#: fas/templates/group/edit.html:44 fas/templates/user/edit.html:74
|
||||
msgid "Save!"
|
||||
msgstr ""
|
||||
|
||||
|
@ -871,7 +929,6 @@ msgid "Approved"
|
|||
msgstr ""
|
||||
|
||||
#: fas/templates/group/list.html:45 fas/templates/group/view.html:21
|
||||
#: fas/templates/user/view.html:57
|
||||
msgid "Unapproved"
|
||||
msgstr ""
|
||||
|
||||
|
@ -891,7 +948,7 @@ msgstr ""
|
|||
msgid "Must Belong To:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/new.html:41 fas/templates/group/view.html:46
|
||||
#: fas/templates/group/new.html:41 fas/templates/group/view.html:47
|
||||
msgid "Join Message:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -911,91 +968,92 @@ msgstr ""
|
|||
msgid "Remove me"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:31 fas/templates/user/view.html:13
|
||||
#: fas/templates/group/view.html:32 fas/templates/user/view.html:13
|
||||
msgid "(edit)"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:34 fas/templates/openid/id.html:16
|
||||
#: fas/templates/group/view.html:35 fas/templates/openid/id.html:16
|
||||
msgid "Name:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:35
|
||||
#: fas/templates/group/view.html:36
|
||||
msgid "Description:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:36
|
||||
#: fas/templates/group/view.html:37
|
||||
msgid "Owner:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:37
|
||||
#: fas/templates/group/view.html:38
|
||||
msgid "Type:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:39 fas/templates/group/view.html:43
|
||||
#: fas/templates/group/view.html:40 fas/templates/group/view.html:44
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:40 fas/templates/group/view.html:44
|
||||
#: fas/templates/group/view.html:41 fas/templates/group/view.html:45
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:47
|
||||
#: fas/templates/group/view.html:48
|
||||
msgid "Prerequisite:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:50
|
||||
#: fas/templates/group/view.html:51
|
||||
msgid "Created:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:58
|
||||
#: fas/templates/group/view.html:59
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:62 fas/templates/user/list.html:27
|
||||
#: fas/templates/group/view.html:63 fas/templates/user/list.html:27
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:63 fas/templates/group/view.html:83
|
||||
#: fas/templates/group/view.html:64 fas/templates/group/view.html:85
|
||||
msgid "Sponsor"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:64
|
||||
#: fas/templates/group/view.html:65
|
||||
msgid "Date Added"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:65
|
||||
#: fas/templates/group/view.html:66
|
||||
msgid "Date Approved"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:66
|
||||
#: fas/templates/group/view.html:67
|
||||
msgid "Approval"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:67
|
||||
#: fas/templates/group/view.html:68
|
||||
msgid "Role Type"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:68
|
||||
#: fas/templates/group/view.html:69
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:74
|
||||
#: fas/templates/group/view.html:75 fas/templates/group/view.html:78
|
||||
#: fas/templates/user/view.html:57
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:84
|
||||
#: fas/templates/group/view.html:87
|
||||
msgid "Approve"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:87
|
||||
#: fas/templates/group/view.html:91
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:90
|
||||
#: fas/templates/group/view.html:95
|
||||
msgid "Upgrade"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/group/view.html:93
|
||||
#: fas/templates/group/view.html:99
|
||||
msgid "Downgrade"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1080,11 +1138,11 @@ msgstr ""
|
|||
msgid "Locale"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/edit.html:68
|
||||
#: fas/templates/user/edit.html:69
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/edit.html:74
|
||||
#: fas/templates/user/edit.html:75
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1100,6 +1158,14 @@ msgstr ""
|
|||
msgid "Account Status"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/list.html:42 fas/templates/user/view.html:30
|
||||
msgid "Signed CLA"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/list.html:43 fas/templates/user/view.html:31
|
||||
msgid "Click-through CLA"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/list.html:44 fas/templates/user/view.html:32
|
||||
msgid "Not Done"
|
||||
msgstr ""
|
||||
|
@ -1112,15 +1178,7 @@ msgstr ""
|
|||
msgid "Email:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/new.html:31 fas/templates/user/view.html:22
|
||||
msgid "Telephone Number:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/new.html:35 fas/templates/user/view.html:23
|
||||
msgid "Postal Address:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/new.html:39
|
||||
#: fas/templates/user/new.html:38
|
||||
msgid "Sign up!"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1170,6 +1228,14 @@ msgstr ""
|
|||
msgid "PGP Key:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/view.html:22
|
||||
msgid "Telephone Number:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/view.html:23
|
||||
msgid "Postal Address:"
|
||||
msgstr ""
|
||||
|
||||
#: fas/templates/user/view.html:24
|
||||
msgid "Comments:"
|
||||
msgstr ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue