diff --git a/fas/README b/fas/README index 66fe76e..834e059 100644 --- a/fas/README +++ b/fas/README @@ -27,7 +27,7 @@ Before you can get started, make sure to have the following packages installed yum install git-core postgresql-plpython postgresql-server postgresql-python \ python-TurboMail TurboGears pygpgme python-sqlalchemy python-genshi \ - python-psycopg2 pytz + python-psycopg2 pytz python-babel babel # Note: on RHEL5 you need postgresql-pl instead of postgresql-plpython @@ -90,6 +90,9 @@ You'll need to edit dev.cfg and change the following lines:: base_url_filter.base_url = "http://localhost:8080/fas" # Change the port if # you changed server.socket_port above. +You may also need to change some of the directories and settings in +fas/config/app.cfg. + You should then be able to start the server and test things out:: ./start-fas.py # browse to http://localhost:8080/fas/ @@ -101,9 +104,9 @@ Make sure you're in the top level directory that start-fas.py and dev.cfg is in, then run:: tg-admin shell -------- +-------------------- Enabling Local Users -------- +-------------------- * THIS IS EXPERIMENTAL * To allow local users to log in to your system, first enable fas via the @@ -125,3 +128,17 @@ example: getent passwd getent group +------------ +Localization +------------ +To generate the POT file (located in the po/ subdirectory), run the +following from the top level directory: + + pybabel extract -F pybabel.conf -o po/fas.pot . +To add a language: tg-admin i18n add +This will create a PO file at po//LC_MESSAGES/fas.po + +To update (merge) PO files with the POT file, run: + + tg-admin i18n merge + diff --git a/fas/client/fasClient.py b/fas/client/fasClient.py index 0e3612e..6dba109 100755 --- a/fas/client/fasClient.py +++ b/fas/client/fasClient.py @@ -162,12 +162,13 @@ class MakeShellAccounts(BaseClient): usernames = {} for person in people: uid = person['id'] - username = person['username'] - usernames[uid] = username - file.write("=%i %s:x:%i:\n" % (uid, username, uid)) - file.write("0%i %s:x:%i:\n" % (i, username, uid)) - file.write(".%s %s:x:%i:\n" % (username, username, uid)) - i = i + 1 + if self.is_valid_user(uid): + username = person['username'] + usernames[uid] = username + file.write("=%i %s:x:%i:\n" % (uid, username, uid)) + file.write("0%i %s:x:%i:\n" % (i, username, uid)) + file.write(".%s %s:x:%i:\n" % (username, username, uid)) + i = i + 1 for group in groups: gid = group['id'] @@ -181,9 +182,9 @@ class MakeShellAccounts(BaseClient): except KeyError: ''' No users exist in the group ''' pass - file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, self.memberships)) - file.write("0%i %s:x:%i:%s\n" % (i, name, gid, self.memberships)) - file.write(".%s %s:x:%i:%s\n" % (name, name, gid, self.memberships)) + file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, memberships)) + file.write("0%i %s:x:%i:%s\n" % (i, name, gid, memberships)) + file.write(".%s %s:x:%i:%s\n" % (name, name, gid, memberships)) i = i + 1 file.close() diff --git a/fas/dev.cfg b/fas/dev.cfg index e266447..417e454 100644 --- a/fas/dev.cfg +++ b/fas/dev.cfg @@ -7,10 +7,9 @@ #mail.server = 'bastion.fedora.phx.redhat.com' #base_url_filter.base_url = "http://192.168.2.101:8080" -fas.url = 'http://localhost:8088/fas/' mail.on = True -mail.server = 'bastion.fedora.phx.redhat.com' -mail.testmode = True +mail.server = 'localhost' +#mail.testmode = True mail.debug = False mail.encoding = 'utf-8' @@ -53,9 +52,9 @@ autoreload.package="fas" # unexpected parameter. False by default tg.strict_parameters = True -server.webpath='/fas' +server.webpath='/accounts' base_url_filter.on=True -base_url_filter.base_url = "http://localhost:8088/fas" +base_url_filter.base_url = "https://publictest10.fedoraproject.org/accounts" # Make the session cookie only return to the host over an SSL link # Disabled for testing. diff --git a/fas/fas/__init__.py b/fas/fas/__init__.py index e69de29..2146c0d 100644 --- a/fas/fas/__init__.py +++ b/fas/fas/__init__.py @@ -0,0 +1,27 @@ +class FASError(Exception): + '''FAS Error''' + pass + +class ApplyError(FASError): + '''Raised when a user could not apply to a group''' + pass + +class ApproveError(FASError): + '''Raised when a user could not be approved in a group''' + pass + +class SponsorError(FASError): + '''Raised when a user could not be sponsored in a group''' + pass + +class UpgradeError(FASError): + '''Raised when a user could not be upgraded in a group''' + pass + +class DowngradeError(FASError): + '''Raised when a user could not be downgraded in a group''' + pass + +class RemoveError(FASError): + '''Raised when a user could not be removed from a group''' + pass diff --git a/fas/fas/auth.py b/fas/fas/auth.py index c3484be..faab6d2 100644 --- a/fas/fas/auth.py +++ b/fas/fas/auth.py @@ -65,14 +65,7 @@ def isApproved(person, group): ''' Returns True if the user is an approved member of a group ''' - try: - role = PersonRoles.query.filter_by(group=group, member=person).one() - except IndexError: - ''' Not in the group ''' - return False - except InvalidRequestError: - return False - if role.role_status == 'approved': + if group in person.approved_memberships: return True else: return False @@ -165,7 +158,7 @@ def canApplyGroup(person, group, applicant): pass else: print "GOT HERE, prereq: %s" % prerequisite - turbogears.flash(_('%s membership required before application to this group is allowed' % prerequisite.name)) + turbogears.flash(_('%s membership required before application to this group is allowed') % prerequisite.name) return False # A user can apply themselves, and FAS admins can apply other people. @@ -173,7 +166,7 @@ def canApplyGroup(person, group, applicant): canAdminGroup(person, group): return True else: - turbogears.flash(_('%s membership required before application to this group is allowed' % prerequisite.name)) + turbogears.flash(_('%s membership required before application to this group is allowed') % prerequisite.name) return False def canSponsorUser(person, group, target): @@ -208,18 +201,16 @@ def canUpgradeUser(person, group, target): ''' Returns True if the user can upgrade target in the group ''' - if isApproved(person, group): - # Group admins can upgrade anybody. - # The controller should handle the case where the target - # is already a group admin. - if canAdminGroup(person, group): - return True - # Sponsors can only upgrade non-sponsors (i.e. normal users) - elif canSponsorGroup(person, group) and \ - not canSponsorGroup(target, group): - return True - else: - return False + # Group admins can upgrade anybody. + # The controller should handle the case where the target + # is already a group admin. + if canAdminGroup(person, group): + return True + # Sponsors can only upgrade non-sponsors (i.e. normal users) + # TODO: Don't assume that canSponsorGroup means that the user is a sponsor + elif canSponsorGroup(person, group) and \ + not canSponsorGroup(target, group): + return True else: return False diff --git a/fas/fas/cla.py b/fas/fas/cla.py index 3b83756..e7abb01 100644 --- a/fas/fas/cla.py +++ b/fas/fas/cla.py @@ -53,14 +53,17 @@ class CLA(controllers.Controller): turbogears.redirect('/user/edit/%s' % username) if type == 'click': - if signedCLAPrivs(person): - turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.')) - turbogears.redirect('/cla/') - return dict() - if clickedCLAPrivs(person): - turbogears.flash(_('You have already completed the Click-through CLA.')) - turbogears.redirect('/cla/') - return dict() + # Disable click-through CLA for now + #if signedCLAPrivs(person): + # turbogears.flash(_('You have already signed the CLA, so it is unnecessary to complete the Click-through CLA.')) + # turbogears.redirect('/cla/') + # return dict() + #if clickedCLAPrivs(person): + # turbogears.flash(_('You have already completed the Click-through CLA.')) + # turbogears.redirect('/cla/') + # return dict() + turbogears.redirect('/cla/') + return dict() elif type == 'sign': if signedCLAPrivs(person): turbogears.flash(_('You have already signed the CLA.')) @@ -99,12 +102,18 @@ class CLA(controllers.Controller): data = StringIO.StringIO(signature.file.read()) plaintext = StringIO.StringIO() verified = False - try: - subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', person.gpg_keyid]) - except subprocess.CalledProcessError: + keyid = re.sub('\s', '', person.gpg_keyid) + ret = subprocess.call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid]) + if ret != 0: turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net")) turbogears.redirect('/cla/view/sign') return dict() + #try: + # subprocess.check_call([config.get('gpgexec'), '--keyserver', config.get('gpg_keyserver'), '--recv-keys', keyid]) + #except subprocess.CalledProcessError: + # turbogears.flash(_("Your key could not be retrieved from subkeys.pgp.net")) + # turbogears.redirect('/cla/view/sign') + # return dict() else: try: sigs = ctx.verify(data, None, plaintext) @@ -116,7 +125,7 @@ class CLA(controllers.Controller): if len(sigs): sig = sigs[0] # This might still assume a full fingerprint. - key = ctx.get_key(re.sub('\s', '', person.gpg_keyid)) + key = ctx.get_key(keyid) fpr = key.subkeys[0].fpr if sig.fpr != fpr: turbogears.flash(_("Your signature's fingerprint did not match the fingerprint registered in FAS.")) @@ -125,7 +134,7 @@ class CLA(controllers.Controller): emails = []; for uid in key.uids: emails.extend([uid.email]) - if person.emails['cla'].email in emails: + if person.emails['primary'].email in emails: verified = True else: turbogears.flash(_('Your key did not match your email.')) @@ -167,13 +176,15 @@ class CLA(controllers.Controller): person.remove(cilckgroup, person) except: pass + # TODO: Email legal-cla-archive@fedoraproject.org turbogears.flash(_("You have successfully signed the CLA. You are now in the '%s' group.") % group.name) turbogears.redirect('/cla/') return dict() @identity.require(turbogears.identity.not_anonymous()) @error_handler(error) - @expose(template="fas.templates.cla.index") + # Don't expose click-through CLA for now. + #@expose(template="fas.templates.cla.index") def click(self, agree): '''Click-through CLA''' username = turbogears.identity.current.user_name diff --git a/fas/fas/client.py b/fas/fas/client.py deleted file mode 100644 index 36e18d1..0000000 --- a/fas/fas/client.py +++ /dev/null @@ -1,228 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2007 Red Hat, Inc. All rights reserved. -# -# This copyrighted material is made available to anyone wishing to use, modify, -# copy, or redistribute it subject to the terms and conditions of the GNU -# General Public License v.2. This program is distributed in the hope that it -# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the -# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the GNU General Public License for more details. You should have -# received a copy of the GNU General Public License along with this program; -# if not, write to the Free Software Foundation, Inc., 51 Franklin Street, -# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are -# incorporated in the source code or documentation are not subject to the GNU -# General Public License and may only be used or replicated with the express -# permission of Red Hat, Inc. -# -# Red Hat Author(s): Luke Macken -# Toshio Kuratomi -# - -''' -python-fedora, python module to interact with Fedora Infrastructure Services -''' - -import Cookie -import urllib -import urllib2 -import logging -import cPickle as pickle -import re -import inspect -import simplejson -from os import path -from urlparse import urljoin - -import gettext -t = gettext.translation('python-fedora', '/usr/share/locale', fallback=True) -_ = t.ugettext - -log = logging.getLogger(__name__) - -SESSION_FILE = path.join(path.expanduser('~'), '.fedora_session') - -class ServerError(Exception): - pass - -class AuthError(ServerError): - pass - -class BaseClient(object): - ''' - A command-line client to interact with Fedora TurboGears Apps. - ''' - def __init__(self, baseURL, username=None, password=None, debug=False): - self.baseURL = baseURL - self.username = username - self.password = password - self._sessionCookie = None - - # Setup our logger - sh = logging.StreamHandler() - if debug: - log.setLevel(logging.DEBUG) - sh.setLevel(logging.DEBUG) - else: - log.setLevel(logging.INFO) - sh.setLevel(logging.INFO) - format = logging.Formatter("%(message)s") - sh.setFormatter(format) - log.addHandler(sh) - - self._load_session() - if username and password: - self._authenticate(force=True) - - def _authenticate(self, force=False): - ''' - Return an authenticated session cookie. - ''' - if not force and self._sessionCookie: - return self._sessionCookie - if not self.username: - raise AuthError, _('username must be set') - if not self.password: - raise AuthError, _('password must be set') - - req = urllib2.Request(urljoin(self.baseURL, 'login?tg_format=json')) - req.add_header('Cookie', self._sessionCookie.output(attrs=[], - header='').strip()) - req.add_data(urllib.urlencode({ - 'user_name' : self.username, - 'password' : self.password, - 'login' : 'Login' - })) - - try: - loginPage = urllib2.urlopen(req) - except urllib2.HTTPError, e: - if e.msg == 'Forbidden': - raise AuthError, _('Invalid username/password') - else: - raise - - loginData = simplejson.load(loginPage) - - if 'message' in loginData: - raise AuthError, _('Unable to login to server: %(message)s') \ - % loginData - - self._sessionCookie = Cookie.SimpleCookie() - try: - self._sessionCookie.load(loginPage.headers['set-cookie']) - except KeyError: - self._sessionCookie = None - raise AuthError, _('Unable to login to the server. Server did' \ - ' not send back a cookie') - self._save_session() - - return self._sessionCookie - session = property(_authenticate) - - def _save_session(self): - ''' - Store our pickled session cookie. - - This method loads our existing session file and modified our - current user's cookie. This allows us to retain cookies for - multiple users. - ''' - save = {} - if path.isfile(SESSION_FILE): - sessionFile = file(SESSION_FILE, 'r') - try: - save = pickle.load(sessionFile) - except: - pass - sessionFile.close() - save[self.username] = self._sessionCookie - sessionFile = file(SESSION_FILE, 'w') - pickle.dump(save, sessionFile) - sessionFile.close() - - def _load_session(self): - ''' - Load a stored session cookie. - ''' - if path.isfile(SESSION_FILE): - sessionFile = file(SESSION_FILE, 'r') - try: - savedSession = pickle.load(sessionFile) - self._sessionCookie = savedSession[self.username] - log.debug(_('Loaded session %(cookie)s') % \ - {'cookie': self._sessionCookie}) - except EOFError: - log.error(_('Unable to load session from %(file)s') % \ - {'file': SESSION_FILE}) - except KeyError: - log.debug(_('Session is for a different user')) - sessionFile.close() - - def send_request(self, method, auth=False, input=None): - ''' - Send a request to the server. The given method is called with any - keyword parameters in **kw. If auth is True, then the request is - made with an authenticated session cookie. - ''' - url = urljoin(self.baseURL, method + '?tg_format=json') - - response = None # the JSON that we get back from the server - data = None # decoded JSON via simplejson.load() - - log.debug(_('Creating request %(url)s') % {'url': url}) - req = urllib2.Request(url) - if input: - req.add_data(urllib.urlencode(input)) - - if auth: - req.add_header('Cookie', self.session.output(attrs=[], - header='').strip()) - elif self._sessionCookie: - # If the cookie exists, send it so that visit tracking works. - req.add_header('Cookie', self._sessionCookie.output(attrs=[], - header='').strip()) - try: - response = urllib2.urlopen(req) - except urllib2.HTTPError, e: - if e.msg == 'Forbidden': - if (inspect.currentframe().f_back.f_code != - inspect.currentframe().f_code): - self._authenticate(force=True) - data = self.send_request(method, auth, input) - else: - # We actually shouldn't ever reach here. Unless something - # goes drastically wrong _authenticate should raise an - # AuthError - raise AuthError, _('Unable to log into server: %(error)s') \ - % {'error': str(e)} - log.error(e) - raise ServerError, str(e) - - # In case the server returned a new session cookie to us - try: - self._sessionCookie.load(response.headers['set-cookie']) - except KeyError: - pass - - try: - data = simplejson.load(response) - except Exception, e: - regex = re.compile('(.*)') - match = regex.search(response) - if match and len(match.groups()): - return dict(tg_flash=match.groups()[0]) - else: - raise ServerError, e.message - - if 'logging_in' in data: - if (inspect.currentframe().f_back.f_code != - inspect.currentframe().f_code): - self._authenticate(force=True) - data = self.send_request(method, auth, input) - else: - # We actually shouldn't ever reach here. Unless something goes - # drastically wrong _authenticate should raise an AuthError - raise AuthError, _('Unable to log into server: %(message)s') \ - % data - return data diff --git a/fas/fas/config/app.cfg b/fas/fas/config/app.cfg index 7bab914..0aafe08 100644 --- a/fas/fas/config/app.cfg +++ b/fas/fas/config/app.cfg @@ -6,6 +6,9 @@ # The commented out values below are the defaults +# Database values +sqlalchemy.convert_unicode=True + admingroup = 'accounts' # VIEW @@ -151,9 +154,11 @@ identity.saprovider.model.group="fas.model.Groups" # identity.saprovider.encryption_algorithm=None accounts_mail = "accounts@fedoraproject.org" +#email_host = "fedoraproject.org" +email_host = "publictest10.fedoraproject.org" gpgexec = "/usr/bin/gpg" -gpghome = "/home/ricky/work/fedora/fedora-infrastructure/fas/gnupg" +gpghome = "/srv/fedora-infrastructure/fas/gnupg" gpg_fingerprint = "C199 1E25 D00A D200 2D2E 54D1 BF7F 1647 C54E 8410" gpg_passphrase = "m00!s@ysth3c0w" gpg_keyserver = "hkp://subkeys.pgp.net" @@ -168,7 +173,7 @@ openidstore = "/var/tmp/fas/openid" openssl_digest = "md5" openssl_expire = 31536000 # 60*60*24*365 = 1 year -openssl_ca_file = "/home/ricky/work/fedora/fedora-infrastructure/fas/ssl/ca-Upload" +openssl_ca_file = "/srv/fedora-infrastructure/fas/ssl/ca-Upload" openssl_c = "US" openssl_st = "North Carolina" openssl_l = "Raleigh" diff --git a/fas/fas/controllers.py b/fas/fas/controllers.py index 8ae178e..674edb8 100644 --- a/fas/fas/controllers.py +++ b/fas/fas/controllers.py @@ -13,6 +13,7 @@ from fas.group import Group from fas.cla import CLA from fas.json_request import JsonRequest from fas.help import Help +from fas.auth import * #from fas.openid_fas import OpenID import os @@ -28,9 +29,14 @@ turbogears.view.variable_providers.append(add_custom_stdvars) def get_locale(locale=None): if locale: return locale - if turbogears.identity.current.user_name: + username = None + try: + username = turbogears.identity.current.user_name + except AttributeError: + pass + if username: person = People.by_username(turbogears.identity.current.user_name) - return person.locale + return person.locale or 'C' else: return turbogears.i18n.utils._get_locale() @@ -58,13 +64,24 @@ class Root(controllers.RootController): @expose(template="fas.templates.welcome", allow_json=True) def index(self): if turbogears.identity.not_anonymous(): + if 'tg_format' in request.params \ + and request.params['tg_format'] == 'json': + # redirects don't work with JSON calls. This is a bit of a + # hack until we can figure out something better. + return dict() turbogears.redirect('/home') return dict(now=time.ctime()) - @expose(template="fas.templates.home") + @expose(template="fas.templates.home", allow_json=True) @identity.require(identity.not_anonymous()) def home(self): - return dict() + user_name = turbogears.identity.current.user_name + person = People.by_username(user_name) + cla = None + if signedCLAPrivs(person): + cla = 'signed' + + return dict(person=person, cla=cla) @expose(template="fas.templates.about") def about(self): @@ -93,7 +110,7 @@ class Root(controllers.RootController): # is better. return dict(user = identity.current.user) if not forward_url: - forward_url = config.get('base_url_filter.base_url') + '/' + forward_url = '/' raise redirect(forward_url) forward_url=None @@ -107,7 +124,7 @@ class Root(controllers.RootController): "this resource.") else: msg=_("Please log in.") - forward_url= request.headers.get("Referer", "/") + forward_url= '/' ### FIXME: Is it okay to get rid of this? #cherrypy.response.status=403 @@ -125,7 +142,7 @@ class Root(controllers.RootController): # redirect to a page. Returning the logged in identity # is better. return dict(status=True) - raise redirect(request.headers.get("Referer", "/")) + raise redirect('/') @expose() def language(self, locale): diff --git a/fas/fas/group.py b/fas/fas/group.py index 8fc685b..f896c77 100644 --- a/fas/fas/group.py +++ b/fas/fas/group.py @@ -4,8 +4,8 @@ from turbogears.database import session import cherrypy +import fas from fas.auth import * - from fas.user import KnownUser import re @@ -37,7 +37,7 @@ class GroupCreate(validators.Schema): name = validators.All( UnknownGroup, validators.String(max=32, min=3), - validators.Regex(regex='^[a-z][a-z0-9]+$'), + validators.Regex(regex='^[a-z0-9\-]+$'), ) display_name = validators.NotEmpty owner = KnownUser @@ -165,8 +165,8 @@ class Group(controllers.Controller): group.display_name = display_name group.owner_id = person_owner.id group.group_type = group_type - group.needs_sponsor = needs_sponsor - group.user_can_remove = user_can_remove + group.needs_sponsor = bool(needs_sponsor) + group.user_can_remove = bool(user_can_remove) if prerequisite: prerequisite = Groups.by_name(prerequisite) group.prerequisite = prerequisite @@ -224,8 +224,8 @@ class Group(controllers.Controller): group.display_name = display_name group.owner = owner group.group_type = group_type - group.needs_sponsor = needs_sponsor - group.user_can_remove = user_can_remove + group.needs_sponsor = bool(needs_sponsor) + group.user_can_remove = bool(user_can_remove) if prerequisite: prerequisite = Groups.by_name(prerequisite) group.prerequisite = prerequisite @@ -279,15 +279,16 @@ class Group(controllers.Controller): else: try: target.apply(group, person) - except: # TODO: More specific exception here. - turbogears.flash(_('%(user)s has already applied to %(group)s!') % \ - {'user': target.username, 'group': group.name}) + except fas.ApplyError, e: + turbogears.flash(_('%(user)s could not apply to %(group)s: %(error)s') % \ + {'user': target.username, 'group': group.name, 'error': e}) + turbogears.redirect('/group/view/%s' % group.name) else: import turbomail - # TODO: How do we handle gettext calls for these kinds of emails? # TODO: CC to right place, put a bit more thought into how to most elegantly do this - message = turbomail.Message(config.get('accounts_mail'), '%s-sponsors@fedoraproject.org' % group.name, \ + # TODO: Maybe that @fedoraproject.org (and even -sponsors) should be configurable somewhere? + message = turbomail.Message(config.get('accounts_mail'), '%(group)s-sponsors@%(host)s' % {'group': group.name, 'host': config.get('email_host')}, \ "Fedora '%(group)s' sponsor needed for %(user)s" % {'user': target.username, 'group': group.name}) url = config.get('base_url_filter.base_url') + turbogears.url('/group/edit/%s' % groupname) @@ -321,8 +322,9 @@ Please go to %(url)s to take action. else: try: target.sponsor(group, person) - except: - turbogears.flash(_("'%s' could not be sponsored!") % target.username) + except fas.SponsorError, e: + turbogears.flash(_("%(user)s could not be sponsored in %(group)s: %(error)s") % \ + {'user': target.username, 'group': group.name, 'error': e}) turbogears.redirect('/group/view/%s' % group.name) else: import turbomail @@ -335,7 +337,7 @@ propagate into the e-mail aliases and CVS repository within an hour. %(joinmsg)s ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg} turbomail.enqueue(message) - turbogears.flash(_("'%s' has been sponsored!") % person.human_name) + turbogears.flash(_("'%s' has been sponsored!") % target.human_name) turbogears.redirect('/group/view/%s' % group.name) return dict() @@ -358,9 +360,9 @@ propagate into the e-mail aliases and CVS repository within an hour. else: try: target.remove(group, target) - except KeyError: - turbogears.flash(_('%(name)s could not be removed from %(group)s!') % \ - {'name': target.username, 'group': group.name}) + except fas.RemoveError, e: + turbogears.flash(_("%(user)s could not be removed from %(group)s: %(error)s") % \ + {'user': target.username, 'group': group.name, 'error': e}) turbogears.redirect('/group/view/%s' % group.name) else: import turbomail @@ -372,7 +374,7 @@ immediately for new operations, and should propagate into the e-mail aliases within an hour. ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email} turbomail.enqueue(message) - turbogears.flash(_('%(name)s has been removed from %(group)s!') % \ + turbogears.flash(_('%(name)s has been removed from %(group)s') % \ {'name': target.username, 'group': group.name}) turbogears.redirect('/group/view/%s' % group.name) return dict() @@ -395,11 +397,9 @@ aliases within an hour. else: try: target.upgrade(group, person) - except TypeError, e: - turbogears.flash(e) - turbogears.redirect('/group/view/%s' % group.name) - except: - turbogears.flash(_('%(name)s could not be upgraded!') % {'name' : target.username}) + except fas.UpgradeError, e: + turbogears.flash(_('%(name)s could not be upgraded in %(group)s: %(error)s') % \ + {'name': target.username, 'group': group.name, 'error': e}) turbogears.redirect('/group/view/%s' % group.name) else: import turbomail @@ -436,8 +436,9 @@ into the e-mail aliases within an hour. else: try: target.downgrade(group, person) - except: - turbogears.flash(_('%(username)s could not be downgraded!') % {'username': target.username}) + except fas.DowngradeError, e: + turbogears.flash(_('%(name)s could not be downgraded in %(group)s: %(error)s') % \ + {'name': target.username, 'group': group.name, 'error': e}) turbogears.redirect('/group/view/%s' % group.name) else: import turbomail @@ -469,7 +470,7 @@ into the e-mail aliases within an hour. turbogears.redirect('/group/list') return dict() else: - return dict(groups=groups) + return dict(group=group) @identity.require(identity.not_anonymous()) @validate(validators=GroupInvite()) diff --git a/fas/fas/help.py b/fas/fas/help.py index 23dd5d6..391eacd 100644 --- a/fas/fas/help.py +++ b/fas/fas/help.py @@ -5,17 +5,25 @@ from turbogears.database import session from fas.auth import * class Help(controllers.Controller): - help = { 'none' : ['Error', '

We could not find that help item

'], - 'user_ircnick' : ['IRC Nick (Optional)', '

IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to

'], - 'user_primary_email' : ['Primary Email (Required)', '

This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded

'], - 'user_human_name' : ['Full Name (Required)', '

Your Human Name or "real life" name

'], - 'user_gpg_keyid' : ['GPG Key', '

Only required for users signing the CLA. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the CLAHowTo for more information

'], - 'user_telephone' : ['Telephone', '

Only required for users signing the CLA. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our Privacy Policy

'], - 'user_postal_address': ['Postal Address', '

Only required for users signing the CLA. This should be a mailing address where you can be contacted. See our Privacy Policy about any concerns.

'], - 'user_timezone': ['Timezone (Optional)', '

Please specify the time zone you are in.

'], - 'user_comments': ['Comments (Optional)', '

Misc comments about yourself.

'], - 'user_account_status': ['Account Status', '

Shows account status, possible values include

  • Valid
  • Disabled
  • Expired

'], - 'user_cla' : ['CLA', '

In order to become a full Fedora contributor you must sign a Contributor License Agreement. This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the CLAHowTo.

'], + help = { 'none' : ['Error', '

We could not find that help item

'], + 'user_ircnick' : ['IRC Nick (Optional)', '

IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to

'], + 'user_primary_email' : ['Primary Email (Required)', '

This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded

'], + 'user_human_name' : ['Full Name (Required)', '

Your Human Name or "real life" name

'], + 'user_gpg_keyid' : ['GPG Key', '

Only required for users signing the CLA. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the CLAHowTo for more information

'], + 'user_telephone' : ['Telephone', '

Only required for users signing the CLA. Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our Privacy Policy

'], + 'user_postal_address': ['Postal Address', '

Only required for users signing the CLA. This should be a mailing address where you can be contacted. See our Privacy Policy about any concerns.

'], + 'user_timezone': ['Timezone (Optional)', '

Please specify the time zone you are in.

'], + 'user_comments': ['Comments (Optional)', '

Misc comments about yourself.

'], + 'user_account_status': ['Account Status', '

Shows account status, possible values include

  • Valid
  • Disabled
  • Expired

'], + 'user_cla' : ['CLA', '

In order to become a full Fedora contributor you must sign a Contributor License Agreement. This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the CLAHowTo.

'], + 'user_locale': ['Locale', '

For non-english speaking peoples this allows individuals to select which locale they are in.

'], + + 'group_apply': ['Apply', '

Applying for a group is like applying for a job and it can certainly take a while to get in. Many groups have their own rules about how to actually get approved or sponsored. For more information on how the account system works see the about page.

'], + 'group_remove': ['Remove', '''

Removing a person from a group will cause that user to no longer be in the group. They will need to re-apply to get in. Admins can remove anyone, Sponsors can remove users, users can't remove anyone.

'''], + 'group_upgrade': ['Upgrade', '''

Upgrade a persons status in this group.

  • from user -> to sponsor
  • From sponsor -> administrator
  • administrators cannot be upgraded beyond administrator

'''], + 'group_downgrade': ['Downgrade', '''

Downgrade a persons status in the group.

  • from administrator -> to sponsor
  • From sponsor -> user
  • users cannot be downgraded below user, you may want to remove them

'''], + 'group_approve': ['Approve', '''

A sponsor or administrator can approve users to be in a group. Once the user has applied for the group, go to the group's page and click approve to approve the user.

'''], + 'group_sponsor': ['Sponsor', '''

A sponsor or administrator can sponsor users to be in a gruop. Once the user has applied for the group, go to the group's page and click approve to sponsor the user. Sponsorship of a user implies that you are approving a user and may mentor and answer their questions as they come up.

'''], } def __init__(self): diff --git a/fas/fas/json.py b/fas/fas/json.py deleted file mode 100644 index 4498c1c..0000000 --- a/fas/fas/json.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2007-2008 Red Hat, Inc. All rights reserved. -# -# This copyrighted material is made available to anyone wishing to use, modify, -# copy, or redistribute it subject to the terms and conditions of the GNU -# General Public License v.2. This program is distributed in the hope that it -# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the -# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the GNU General Public License for more details. You should have -# received a copy of the GNU General Public License along with this program; -# if not, write to the Free Software Foundation, Inc., 51 Franklin Street, -# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are -# incorporated in the source code or documentation are not subject to the GNU -# General Public License and may only be used or replicated with the express -# permission of Red Hat, Inc. -# -# Red Hat Author(s): Toshio Kuratomi -# - -''' -JSON Helper functions. Most JSON code directly related to classes is -implemented via the __json__() methods in model.py. These methods define -methods of transforming a class into json for a few common types. -''' -# A JSON-based API(view) for your app. -# Most rules would look like: -# @jsonify.when("isinstance(obj, YourClass)") -# def jsonify_yourclass(obj): -# return [obj.val1, obj.val2] -# @jsonify can convert your objects to following types: -# lists, dicts, numbers and strings - -import sqlalchemy -from turbojson.jsonify import jsonify - -class SABase(object): - '''Base class for SQLAlchemy mapped objects. - - This base class makes sure we have a __json__() method on each SQLAlchemy - mapped object that knows how to:: - - 1) return json for the object. - 2) Can selectively add tables pulled in from the table to the data we're - returning. - ''' - # pylint: disable-msg=R0903 - def __json__(self): - '''Transform any SA mapped class into json. - - This method takes an SA mapped class and turns the "normal" python - attributes into json. The properties (from properties in the mapper) - are also included if they have an entry in jsonProps. You make - use of this by setting jsonProps in the controller. - - Example controller:: - john = model.Person.get_by(name='John') - # Person has a property, addresses, linking it to an Address class. - # Address has a property, phone_nums, linking it to a Phone class. - john.jsonProps = {'Person': ['addresses'], - 'Address': ['phone_nums']} - return dict(person=john) - - jsonProps is a dict that maps class names to lists of properties you - want to output. This allows you to selectively pick properties you - are interested in for one class but not another. You are responsible - for avoiding loops. ie: *don't* do this:: - john.jsonProps = {'Person': ['addresses'], 'Address': ['people']} - ''' - props = {} - # pylint: disable-msg=E1101 - if 'jsonProps' in self.__dict__ \ - and self.jsonProps.has_key(self.__class__.__name__): - propList = self.jsonProps[self.__class__.__name__] - else: - propList = {} - # pylint: enable-msg=E1101 - - # Load all the columns from the table - for column in sqlalchemy.orm.object_mapper(self).iterate_properties: - if isinstance(column, sqlalchemy.orm.properties.ColumnProperty): - props[column.key] = getattr(self, column.key) - - # Load things that are explicitly listed - for field in propList: - props[field] = getattr(self, field) - try: - # pylint: disable-msg=E1101 - props[field].jsonProps = self.jsonProps - except AttributeError: # pylint: disable-msg=W0704 - # Certain types of objects are terminal and won't allow setting - # jsonProps - pass - return props - -@jsonify.when("isinstance(obj, sqlalchemy.orm.query.Query)") -def jsonify_sa_select_results(obj): - '''Transform selectresults into lists. - - The one special thing is that we bind the special jsonProps into each - descendent. This allows us to specify a jsonProps on the toplevel - query result and it will pass to all of its children. - ''' - if 'jsonProps' in obj.__dict__: - for element in obj: - element.jsonProps = obj.jsonProps - return list(obj) - -@jsonify.when("isinstance(obj, sqlalchemy.orm.attributes.InstrumentedAttribute) or isinstance(obj, sqlalchemy.ext.associationproxy._AssociationList)") -def jsonify_salist(obj): - '''Transform SQLAlchemy InstrumentedLists into json. - - The one special thing is that we bind the special jsonProps into each - descendent. This allows us to specify a jsonProps on the toplevel - query result and it will pass to all of its children. - ''' - if 'jsonProps' in obj.__dict__: - for element in obj: - element.jsonProps = obj.jsonProps - return [jsonify(element) for element in obj] diff --git a/fas/fas/jsonfasprovider.py b/fas/fas/jsonfasprovider.py deleted file mode 100644 index 73c09b6..0000000 --- a/fas/fas/jsonfasprovider.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2007-2008 Red Hat, Inc. All rights reserved. -# -# This copyrighted material is made available to anyone wishing to use, modify, -# copy, or redistribute it subject to the terms and conditions of the GNU -# General Public License v.2. This program is distributed in the hope that it -# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the -# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the GNU General Public License for more details. You should have -# received a copy of the GNU General Public License along with this program; -# if not, write to the Free Software Foundation, Inc., 51 Franklin Street, -# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are -# incorporated in the source code or documentation are not subject to the GNU -# General Public License and may only be used or replicated with the express -# permission of Red Hat, Inc. -# -# Red Hat Author(s): Toshio Kuratomi -# - -''' -This plugin provides integration with the Fedora Account -System using JSON calls. -''' - -import Cookie - -from cherrypy import request -from sqlalchemy.orm import class_mapper -from turbogears import config, identity -from turbogears.identity.saprovider import SqlAlchemyIdentity, \ - SqlAlchemyIdentityProvider -from turbogears.database import session -from turbogears.util import load_class - -# Once this works, propogate the changes back to python-fedora and import as -# from fedora.tg.client import BaseClient -from client import BaseClient - -import gettext -t = gettext.translation('python-fedora', '/usr/share/locale', fallback=True) -_ = t.ugettext - -import crypt - -import logging -log = logging.getLogger('turbogears.identity.safasprovider') - -try: - set, frozenset -except NameError: - from sets import Set as set, ImmutableSet as frozenset - -class JsonFasIdentity(BaseClient): - '''Associate an identity with a person in the auth system. - ''' - cookieName = config.get('visit.cookie.name', 'tg-visit') - fasURL = config.get('fas.url', 'https://admin.fedoraproject.org/admin/fas/') - - def __init__(self, visit_key, user=None, username=None, password=None, - debug=False): - super(JsonFasIdentity, self).__init__(self.fasURL, debug=debug) - if user: - self._user = user - self._groups = frozenset( - [g['name'] for g in data['person']['approved_memberships']] - ) - self.visit_key = visit_key - # It's allowed to use a null value for a visit_key if we know we're - # generating an anonymous user. The json interface doesn't handle - # that, though, and there's no reason for us to make it. - if not visit_key: - return - - # Set the cookie to the user's tg_visit key before requesting - # authentication. That way we link the two together. - self._sessionCookie = Cookie.SimpleCookie() - self._sessionCookie[self.cookieName] = self.visit_key - self.username = username - self.password = password - if username and password: - self._authenticate(force=True) - - def _authenticate(self, force=False): - '''Override BaseClient so we can keep visit_key in sync. - ''' - super(JsonFasIdentity, self)._authenticate(force) - if self._sessionCookie[self.cookieName].value != self.visit_key: - # When the visit_key changes (because the old key had expired or - # been deleted from the db) change the visit_key in our variables - # and the session cookie to be sent back to the client. - self.visit_key = self._sessionCookie[self.cookieName].value - cookies = request.simple_cookie - cookies[self.cookieName] = self.visit_key - return self._sessionCookie - session = property(_authenticate) - - def _get_user(self): - '''Retrieve information about the user from cache or network.''' - try: - return self._user - except AttributeError: - # User hasn't already been set - pass - # Attempt to load the user. After this code executes, there *WILL* be - # a _user attribute, even if the value is None. - # Query the account system URL for our given user's sessionCookie - # FAS returns user and group listing - data = self.send_request('user/view', auth=True) - if not data['person']: - self._user = None - return None - self._user = data['person'] - self._groups = frozenset( - [g['name'] for g in data['person']['approved_memberships']] - ) - return self._user - user = property(_get_user) - - def _get_user_name(self): - if not self.user: - return None - return self.user['username'] - user_name = property(_get_user_name) - - def _get_groups(self): - try: - return self._groups - except AttributeError: - # User and groups haven't been returned. Since the json call - # returns both user and groups, this is set at user creation time. - self._groups = frozenset() - return self._groups - groups = property(_get_groups) - - def logout(self): - ''' - Remove the link between this identity and the visit. - ''' - if not self.visit_key: - return - # Call Account System Server logout method - self.send_request('logout', auth=True) - -class JsonFasIdentityProvider(object): - ''' - IdentityProvider that authenticates users against the fedora account system - ''' - def __init__(self): - # Default encryption algorithm is to use plain text passwords - algorithm = config.get("identity.saprovider.encryption_algorithm", None) - self.encrypt_password = lambda pw: \ - identity._encrypt_password(algorithm, pw) - - def create_provider_model(self): - ''' - Create the database tables if they don't already exist. - ''' - # No database tables to create because the db is behind the FAS2 - # server - pass - - def validate_identity(self, user_name, password, visit_key): - ''' - Look up the identity represented by user_name and determine whether the - password is correct. - - Must return either None if the credentials weren't valid or an object - with the following properties: - user_name: original user name - user: a provider dependant object (TG_User or similar) - groups: a set of group IDs - permissions: a set of permission IDs - ''' - try: - user = JsonFasIdentity(visit_key, username=user_name, - password=password) - except AuthError, e: - log.warning('Error logging in %(user)s: %(error)s' % { - 'user': username, 'error': e}) - return None - - return JsonFasIdentity(visit_key, user) - - def validate_password(self, user, user_name, password): - ''' - Check the supplied user_name and password against existing credentials. - Note: user_name is not used here, but is required by external - password validation schemes that might override this method. - If you use SqlAlchemyIdentityProvider, but want to check the passwords - against an external source (i.e. PAM, LDAP, Windows domain, etc), - subclass SqlAlchemyIdentityProvider, and override this method. - - Arguments: - :user: User information. Not used. - :user_name: Given username. - :password: Given, plaintext password. - - Returns: True if the password matches the username. Otherwise False. - Can return False for problems within the Account System as well. - ''' - - return user.password == crypt.crypt(password, user.password) - - def load_identity(self, visit_key): - '''Lookup the principal represented by visit_key. - - Arguments: - :visit_key: The session key for whom we're looking up an identity. - - Must return an object with the following properties: - user_name: original user name - user: a provider dependant object (TG_User or similar) - groups: a set of group IDs - permissions: a set of permission IDs - ''' - return JsonFasIdentity(visit_key) - - def anonymous_identity(self): - ''' - Must return an object with the following properties: - user_name: original user name - user: a provider dependant object (TG_User or similar) - groups: a set of group IDs - permissions: a set of permission IDs - ''' - - return JsonFasIdentity(None) - - def authenticated_identity(self, user): - ''' - Constructs Identity object for user that has no associated visit_key. - ''' - return JsonFasIdentity(None, user) diff --git a/fas/fas/jsonfasvisit.py b/fas/fas/jsonfasvisit.py deleted file mode 100644 index 0bdd38a..0000000 --- a/fas/fas/jsonfasvisit.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -This plugin provides integration with the Fedora Account System using JSON -calls to the account system server. -''' - -import Cookie - -from datetime import datetime - -from sqlalchemy import * -from sqlalchemy.orm import class_mapper - -from turbogears import config -from turbogears.visit.api import BaseVisitManager, Visit -from turbogears.database import get_engine, metadata, session, mapper -from turbogears.util import load_class - -# Once this works, propogate the changes back to python-fedora and import as -# from fedora.tg.client import BaseClient -from client import BaseClient - -import gettext -t = gettext.translation('python-fedora', '/usr/share/locale', fallback=True) -_ = t.ugettext - -import logging -log = logging.getLogger("turbogears.identity.savisit") - -class JsonFasVisitManager(BaseClient): - ''' - This proxies visit requests to the Account System Server running remotely. - - We don't need to worry about threading and other concerns because our proxy - doesn't cause any asynchronous calls. - ''' - fasURL = config.get('fas.url', 'https://admin.fedoraproject.org/admin/fas') - cookieName = config.get('visit.cookie.name', 'tg-visit') - - def __init__(self, timeout, debug=None): - super(JsonFasVisitManager,self).__init__(self.fasURL, debug=debug) - - def create_model(self): - ''' - Create the Visit table if it doesn't already exist - ''' - # Not needed as the visit tables reside remotely in the FAS2 database. - pass - - def new_visit_with_key(self, visit_key): - # Hit any URL in fas2 with the visit_key set. That will call the - # new_visit method in fas2 - self._sessionCookie = Cookie.SimpleCookie() - self._sessionCookie[self.cookieName] = visit_key - data = self.send_request('', auth=True) - return Visit(self._sessionCookie[self.cookieName].value, True) - - def visit_for_key(self, visit_key): - ''' - Return the visit for this key or None if the visit doesn't exist or has - expired. - ''' - # Hit any URL in fas2 with the visit_key set. That will call the - # new_visit method in fas2 - self._sessionCookie = Cookie.SimpleCookie() - self._sessionCookie[self.cookieName] = visit_key - data = self.send_request('', auth=True) - # Knowing what happens in turbogears/visit/api.py when this is called, - # we can shortcircuit this step and avoid a round trip to the FAS - # server. - # if visit_key != self._sessionCookie[self.cookieName].value: - # # visit has expired - # return None - # # Hitting FAS has already updated the visit. - # return Visit(visit_key, False) - if visit_key != self._sessionCookie[self.cookieName].value: - return Visit(self._sessionCookie[self.cookieName].value, True) - else: - return Visit(visit_key, False) - - def update_queued_visits(self, queue): - # Let the visit_manager on the FAS server manage this - pass diff --git a/fas/fas/model.py b/fas/fas/model.py index bd02b6b..97bb28b 100644 --- a/fas/fas/model.py +++ b/fas/fas/model.py @@ -42,10 +42,10 @@ from turbogears.database import session from turbogears import identity -from fas.json import SABase +import turbogears -# Soon we'll use this instead: -#from fedora.tg.json import SABase +from fedora.tg.json import SABase +import fas # Bind us to the database defined in the config file. get_engine() @@ -121,18 +121,21 @@ class People(SABase): ''' Apply a person to a group ''' - role = PersonRoles() - role.role_status = 'unapproved' - role.role_type = 'user' - role.member = cls - role.group = group + if group in cls.memberships: + raise fas.ApplyError, _('user is already in this group') + else: + role = PersonRoles() + role.role_status = 'unapproved' + role.role_type = 'user' + role.member = cls + role.group = group def approve(cls, group, requester): ''' Approve a person in a group - requester for logging purposes ''' - if group in cls.approved_memberships: - raise '%s is already approved in %s' % (cls.username, group.name) + if group not in cls.unapproved_memberships: + raise fas.ApproveError, _('user is not an unapproved member') else: role = PersonRoles.query.filter_by(member=cls, group=group).one() role.role_status = 'approved' @@ -142,11 +145,11 @@ class People(SABase): Upgrade a user in a group - requester for logging purposes ''' if not group in cls.memberships: - raise '%s not a member of %s' % (group.name, cls.memberships) + raise fas.UpgradeError, _('user is not a member') else: role = PersonRoles.query.filter_by(member=cls, group=group).one() if role.role_type == 'administrator': - raise '%s is already an admin in %s' % (cls.username, group.name) + raise fas.UpgradeError, _('administrators cannot be upgraded any further') elif role.role_type == 'sponsor': role.role_type = 'administrator' elif role.role_type == 'user': @@ -157,11 +160,11 @@ class People(SABase): Downgrade a user in a group - requester for logging purposes ''' if not group in cls.memberships: - raise '%s not a member of %s' % (group.name, cls.memberships) + raise fas.DowngradeError, _('user is not a member') else: role = PersonRoles.query.filter_by(member=cls, group=group).one() if role.role_type == 'user': - raise '%s is already a user in %s, did you mean to remove?' % (cls.username, group.name) + raise fas.DowngradeError, _('users cannot be downgraded any further') elif role.role_type == 'sponsor': role.role_type = 'user' elif role.role_type == 'administrator': @@ -169,20 +172,19 @@ class People(SABase): def sponsor(cls, group, requester): # If we want to do logging, this might be the place. - if not group in cls.memberships: - raise '%s not a member of %s' % (group.name, cls.memberships) + if not group in cls.unapproved_memberships: + raise fas.SponsorError, _('user is not an unapproved member') role = PersonRoles.query.filter_by(member=cls, group=group).one() role.role_status = 'approved' role.sponsor_id = requester.id role.approval = datetime.now(pytz.utc) def remove(cls, group, requester): - role = PersonRoles.query.filter_by(member=cls, group=group).one() - try: + if not group in cls.memberships: + raise fas.RemoveError, _('user is not a member') + else: + role = PersonRoles.query.filter_by(member=cls, group=group).one() session.delete(role) - except TypeError: - pass - # Handle somehow. def __repr__(cls): return "User(%s,%s)" % (cls.username, cls.human_name) diff --git a/fas/fas/openid_fas.py b/fas/fas/openid_fas.py index e3d6635..f9ca8c7 100644 --- a/fas/fas/openid_fas.py +++ b/fas/fas/openid_fas.py @@ -106,7 +106,7 @@ class OpenID(controllers.Controller): @validate(validators=UserID()) def id(self, username): '''The "real" OpenID URL''' - person = Person.by_username(username) + person = People.by_username(username) server = config.get('base_url') + turbogears.url('/openid/server') return dict(person=person, server=server) diff --git a/fas/fas/static/css/style.css b/fas/fas/static/css/style.css index e861567..daece0c 100644 --- a/fas/fas/static/css/style.css +++ b/fas/fas/static/css/style.css @@ -25,7 +25,7 @@ a margin-top: 35px; height: 70px; line-height: 70px; - background: url(/fas/static/images/head.png) 0 0 repeat-x; + background: url(../images/head.png) 0 0 repeat-x; } #head h1 @@ -34,7 +34,7 @@ a float: left; text-indent: -9999px; overflow: hidden; - background: url(/fas/static/images/logo.png) 1ex 50% no-repeat; + background: url(../images/logo.png) 1ex 50% no-repeat; } #searchbox @@ -65,7 +65,7 @@ a { height: 30px; line-height: 30px; - background: url(/fas/static/images/topnav.png) 0 0 repeat-x; + background: url(../images/topnav.png) 0 0 repeat-x; font-size: 1.6ex; } @@ -78,7 +78,7 @@ a #topnav ul li { display: inline; - background: url(/fas/static/images/topnav-separator.png) 0 50% no-repeat; + background: url(../images/topnav-separator.png) 0 50% no-repeat; padding-left: 3px; } @@ -106,7 +106,7 @@ a right: 0; height: 35px; line-height: 35px; - background: url(/fas/static/images/infobar.png) 0 0 repeat-x; + background: url(../images/infobar.png) 0 0 repeat-x; font-size: 1.6ex; } @@ -139,7 +139,7 @@ a #control ul li { display: inline; - background: url(/fas/static/images/control-separator.png) 0 50% no-repeat; + background: url(../images/control-separator.png) 0 50% no-repeat; } #control a @@ -150,14 +150,14 @@ a #main { - background: url(/fas/static/images/shadow.png) 0 0 repeat-x; + background: url(../images/shadow.png) 0 0 repeat-x; } #sidebar { width: 22ex; float: left; - background: #335F9D url(/fas/static/images/sidebar.png) 0 0 repeat-x; + background: #335F9D url(../images/sidebar.png) 0 0 repeat-x; border: 1px solid #112233; } @@ -246,25 +246,25 @@ a .account { padding-left: 30px; - background: url(/fas/static/images/account.png) 0 68% no-repeat; + background: url(../images/account.png) 0 68% no-repeat; } .approved { padding-left: 20px; - background: url(/fas/static/images/approved.png) 0 68% no-repeat; + background: url(../images/approved.png) 0 68% no-repeat; } .unapproved { padding-left: 20px; - background: url(/fas/static/images/unapproved.png) 0 68% no-repeat; + background: url(../images/unapproved.png) 0 68% no-repeat; } .attn { padding-left: 20px; - background: url(/fas/static/images/attn.png) 0 68% no-repeat; + background: url(../images/attn.png) 0 68% no-repeat; } .roleslist @@ -302,7 +302,7 @@ a margin-top: 1ex; padding-top: 1ex; padding-left: 22px; - background: url(/fas/static/images/arrow.png) 0 1.6ex no-repeat; + background: url(../images/arrow.png) 0 1.6ex no-repeat; } #rolespanel h4 @@ -332,13 +332,13 @@ a #rolespanel .tools li { padding-left: 22px; - background: url(/fas/static/images/tools.png) 0 50% no-repeat; + background: url(../images/tools.png) 0 50% no-repeat; } #rolespanel .queue li { padding-left: 22px; - background: url(/fas/static/images/queue.png) 0 50% no-repeat; + background: url(../images/queue.png) 0 50% no-repeat; } #rolespanel .queue strong @@ -352,7 +352,7 @@ a clear: both; text-align: center; padding: 15px 0 2.5ex; - background: url(/fas/static/images/footer-top.png) 0 0 repeat-x; + background: url(../images/footer-top.png) 0 0 repeat-x; } #footer .copy, #footer .disclaimer @@ -364,7 +364,7 @@ a { padding-top: 3px; padding-bottom: 18px; - background: #EEEEEE url(/fas/static/images/footer-bottom.png) 0 100% repeat-x; + background: #EEEEEE url(../images/footer-bottom.png) 0 100% repeat-x; list-style: none; } @@ -389,7 +389,7 @@ a .flash { - background: #DEE6B1 url(/fas/static/images/success.png) 10px 50% no-repeat; + background: #DEE6B1 url(../images/success.png) 10px 50% no-repeat; border: 1px solid #CCBBAA; padding: 1.5ex 15px 1.5ex 43px; margin: 1ex 0; @@ -397,7 +397,7 @@ a .help { - background: #DEE6B1 url(/fas/static/images/help.png) 10px 50% no-repeat; + background: #DEE6B1 url(../images/help.png) 10px 50% no-repeat; border: 1px solid #CCBBAA; padding: 1.5ex 15px 1.5ex 65px; margin: 1ex 0; diff --git a/fas/fas/static/js/HelpBalloon.js b/fas/fas/static/js/HelpBalloon.js index 4f19892..cb8e7d8 100644 --- a/fas/fas/static/js/HelpBalloon.js +++ b/fas/fas/static/js/HelpBalloon.js @@ -717,7 +717,7 @@ HelpBalloonOptions.prototype = { * to an existing element if you're using that as your anchoring icon. * @var {Object} */ - icon: '/static/images/balloons/icon.gif', + icon: '/accounts/static/images/balloons/icon.gif', /** * Alt text of the help icon * @var {String} @@ -783,7 +783,7 @@ HelpBalloonOptions.prototype = { * Clossing button image path * @var {String} */ - button: '/static/images/balloons/button.png', + button: '/accounts/static/images/balloons/button.png', /** * Balloon image path prefix. There are 4 button images, numerically named, starting with 0. * 0, 1 @@ -791,7 +791,7 @@ HelpBalloonOptions.prototype = { * (the number indicates the corner opposite the anchor (the pointing direction) * @var {String} */ - balloonPrefix: '/static/images/balloons/balloon-', + balloonPrefix: '/accounts/static/images/balloons/balloon-', /** * The image filename suffix, including the file extension * @var {String} diff --git a/fas/fas/templates/about.html b/fas/fas/templates/about.html index c27fe5e..d6dbd9f 100644 --- a/fas/fas/templates/about.html +++ b/fas/fas/templates/about.html @@ -8,10 +8,10 @@

${_('FAS - The Open Account System')}

-

${_('FAS is designed around an open architecture. Unlike the traditional account systems where a single admin or group of admins decide who gets to be in what group, FAS is completely designed to be self operating per team. Every group is given at least one administrator who can then approve other people in the group. Also, unlike traditional account systems. FAS allows people to apply for the groups they want to be in. This paridigm is interesting as it allows anyone to find out who is in what groups and contact them. This openness is brought over from the same philosophies that make Open Source popular.')}

+

${_('''FAS is designed around an open architecture. Unlike the traditional account systems where a single admin or group of admins decide who gets to be in what group, FAS is completely designed to be self operating per team. Every group is given at least one administrator who can then approve other people in the group. Also, unlike traditional account systems. FAS allows people to apply for the groups they want to be in. This paridigm is interesting as it allows anyone to find out who is in what groups and contact them. This openness is brought over from the same philosophies that make Open Source popular.''')}

${_('Etiquette')}

-

${_('People shouldn't assume that by applying for a group that they're then in that group. Consider it like applying for another job. It often takes time. For best odds of success, learn about the group you're applying for and get to know someone in the group. Find someone with sponsor or admin access and ask them if they'd have time to mentor you. Plan on spending at least a few days learning about the group, doing a mundain task, participating on the mailing list. Sometimes this process can take weeks depending on the group. It's best to know you will get sponsored before you apply.')} +

${_("People shouldn't assume that by applying for a group that they're then in that group. Consider it like applying for another job. It often takes time. For best odds of success, learn about the group you're applying for and get to know someone in the group. Find someone with sponsor or admin access and ask them if they'd have time to mentor you. Plan on spending at least a few days learning about the group, doing a mundain task, participating on the mailing list. Sometimes this process can take weeks depending on the group. It's best to know you will get sponsored before you apply.")}

${_('Users, Sponsors, Administrators')}

-

${_('Once you're in the group, you're in the group. Sponsorship and Administrators typically have special access in the group in questions. Some groups consider sponsorship level to be of a higher involvement, partial ownership of the group for example. But as far as the account system goes the disctinction is easy. Sponsors can approve new users and make people into sponsors. They cannot, however, downgrade or remove other sponsors. They also cannot change administrators in any way. Administrators can do anything to anyone in the group.')} +

${_('''Once you're in the group, you're in the group. Sponsorship and Administrators typically have special access in the group in questions. Some groups consider sponsorship level to be of a higher involvement, partial ownership of the group for example. But as far as the account system goes the disctinction is easy. Sponsors can approve new users and make people into sponsors. They cannot, however, downgrade or remove other sponsors. They also cannot change administrators in any way. Administrators can do anything to anyone in the group.''')}

diff --git a/fas/fas/templates/cla/index.html b/fas/fas/templates/cla/index.html index 31fd226..40396d4 100644 --- a/fas/fas/templates/cla/index.html +++ b/fas/fas/templates/cla/index.html @@ -9,6 +9,7 @@

${_('Fedora Contributor License Agreement')}

+ ${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.'))}


diff --git a/fas/fas/templates/group/dump.txt b/fas/fas/templates/group/dump.txt index fb68b9a..5922563 100644 --- a/fas/fas/templates/group/dump.txt +++ b/fas/fas/templates/group/dump.txt @@ -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 diff --git a/fas/fas/templates/group/list.html b/fas/fas/templates/group/list.html index bd925ae..094d3ba 100644 --- a/fas/fas/templates/group/list.html +++ b/fas/fas/templates/group/list.html @@ -45,6 +45,7 @@ ${_('Unapproved')} ${_('Apply')} + diff --git a/fas/fas/templates/group/new.html b/fas/fas/templates/group/new.html index 4ba85ab..7f93f8d 100644 --- a/fas/fas/templates/group/new.html +++ b/fas/fas/templates/group/new.html @@ -27,15 +27,15 @@
- +
- +
- +
diff --git a/fas/fas/templates/group/view.html b/fas/fas/templates/group/view.html index 318eaa8..cfa0a1f 100644 --- a/fas/fas/templates/group/view.html +++ b/fas/fas/templates/group/view.html @@ -28,6 +28,7 @@
${_('Remove me')} +

Group Details ${_('(edit)')}

@@ -70,10 +71,11 @@ ${role.member.username} - ${role.member.username} - ${_('None')} + ${role.sponsor.username} + ${_('None')} ${role.creation.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')} - ${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')} + ${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')} + ${_('None')} ${role.role_status} ${role.role_type} @@ -81,16 +83,21 @@
  • ${_('Sponsor')} + ${_('Approve')} +
  • ${_('Remove')} +
  • ${_('Upgrade')} +
  • ${_('Downgrade')} +
diff --git a/fas/fas/templates/home.html b/fas/fas/templates/home.html index 0013343..c64eb8b 100644 --- a/fas/fas/templates/home.html +++ b/fas/fas/templates/home.html @@ -7,5 +7,25 @@ ${_('Fedora Accounts System')} + +

Todo queue:

+ +
+ +
+
    +
  • + ${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})} +
  • +
+
+
+
+
+ diff --git a/fas/fas/templates/master.html b/fas/fas/templates/master.html index c11c269..d04b222 100644 --- a/fas/fas/templates/master.html +++ b/fas/fas/templates/master.html @@ -46,7 +46,7 @@
    -
  • about
  • +
  • About
  • ${_('My Account')}
  • ${_('Log Out')}
  • ${_('Log In')}
  • @@ -56,13 +56,14 @@
    -
    +
    +
    Warning: This is a test instance!
    +
      +
    • Avoid entering private info here.
    • +
    • The database may be wiped/rebuilt periodically.
    • +
    • Feel free to file bugs, enhancements, etc. at https://fedorahosted.org/fas2/
    • +
    +
    +
    ${tg_flash}
    - +
    diff --git a/fas/fas/templates/user/view.html b/fas/fas/templates/user/view.html index ced3f25..9fa786b 100644 --- a/fas/fas/templates/user/view.html +++ b/fas/fas/templates/user/view.html @@ -19,18 +19,21 @@
    ${_('IRC Nick:')}
    ${person.ircnick} 
    ${_('PGP Key:')}
    ${person.gpg_keyid} 
    -
    ${_('Telephone Number:')}
    ${person.telephone} 
    -
    ${_('Postal Address:')}
    ${person.postal_address} 
    +
    ${_('Telephone Number:')}
    ${person.telephone} 
    +
    ${_('Postal Address:')}
    ${person.postal_address} 
    ${_('Comments:')}
    ${person.comments} 
    -
    ${_('Password:')}
    ${_('Valid')} (change)
    -
    ${_('Account Status:')}
    ${_('Valid')} -
    - +
    ${_('Password:')}
    ${_('Valid')} (change)
    +
    ${_('Account Status:')}
    + ${_('Active')} + ${_('Vacation')} + ${_('Inactive')} + ${_('Pinged')} +
    ${_('CLA:')}
    ${_('Signed CLA')} ${_('Click-through CLA')}(${_('GPG Sign it!')}) ${_('Not Done')} (${_('Sign it!')}) -
    +

    ${_('Your Roles')}

    @@ -54,7 +57,7 @@
    ${_('Status:')}
    ${_('Approved')} - ${_('Unapproved')} + ${_('None')}
    ${_('Tools:')}
    diff --git a/fas/fas/user.py b/fas/fas/user.py index d209b68..c6831f0 100644 --- a/fas/fas/user.py +++ b/fas/fas/user.py @@ -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') diff --git a/fas/fas2.sql b/fas/fas2.sql index e19dfe9..a938c24 100644 --- a/fas/fas2.sql +++ b/fas/fas2.sql @@ -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', diff --git a/fas/po/fas.pot b/fas/po/fas.pot index e77ce5a..4344b4d 100644 --- a/fas/po/fas.pot +++ b/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 \n" "Language-Team: LANGUAGE \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 websites team." 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 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 CLA." 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 ""