From a1283c3a1cb6dc3dedd75936809affe1bc9c597c Mon Sep 17 00:00:00 2001 From: Ricky Zhou Date: Thu, 16 Aug 2007 13:40:01 -0700 Subject: [PATCH] Split user/group into separate files, convert to genshi for templates. TODO: Form validation --- fas/fas/auth.py | 52 ++ fas/fas/config/app.cfg | 1 + fas/fas/controllers.py | 482 +----------------- fas/fas/group.py | 280 ++++++++++ fas/fas/static/css/style.css | 9 +- fas/fas/templates/autoComplete.kid | 3 - fas/fas/templates/dump.kid | 7 - fas/fas/templates/editAccount.kid | 10 - fas/fas/templates/editGroup.kid | 10 - fas/fas/templates/error.kid | 9 - fas/fas/templates/group/__init__.py | 0 fas/fas/templates/group/__init__.pyc | Bin 0 -> 156 bytes fas/fas/templates/group/dump.txt | 3 + fas/fas/templates/group/edit.html | 39 ++ fas/fas/templates/group/list.html | 44 ++ fas/fas/templates/group/new.html | 41 ++ fas/fas/templates/group/view.html | 87 ++++ fas/fas/templates/groupList.kid | 46 -- fas/fas/templates/{home.kid => home.html} | 8 +- fas/fas/templates/{invite.kid => invite.html} | 9 +- fas/fas/templates/login.html | 33 ++ fas/fas/templates/login.kid | 33 -- fas/fas/templates/{master.kid => master.html} | 25 +- fas/fas/templates/resetPassword.kid | 30 -- fas/fas/templates/signUp.kid | 10 - fas/fas/templates/user/__init__.py | 0 fas/fas/templates/user/__init__.pyc | Bin 0 -> 155 bytes fas/fas/templates/user/changepass.html | 20 + fas/fas/templates/user/edit.html | 51 ++ .../{userList.kid => user/list.html} | 18 +- fas/fas/templates/user/new.html | 37 ++ fas/fas/templates/user/resetpass.html | 19 + .../{viewAccount.kid => user/view.html} | 34 +- fas/fas/templates/viewGroup.kid | 84 --- .../templates/{welcome.kid => welcome.html} | 9 +- fas/fas/user.py | 253 +++++++++ 36 files changed, 1031 insertions(+), 765 deletions(-) create mode 100644 fas/fas/auth.py create mode 100644 fas/fas/group.py delete mode 100644 fas/fas/templates/autoComplete.kid delete mode 100644 fas/fas/templates/dump.kid delete mode 100644 fas/fas/templates/editAccount.kid delete mode 100644 fas/fas/templates/editGroup.kid delete mode 100644 fas/fas/templates/error.kid create mode 100644 fas/fas/templates/group/__init__.py create mode 100644 fas/fas/templates/group/__init__.pyc create mode 100644 fas/fas/templates/group/dump.txt create mode 100644 fas/fas/templates/group/edit.html create mode 100644 fas/fas/templates/group/list.html create mode 100644 fas/fas/templates/group/new.html create mode 100644 fas/fas/templates/group/view.html delete mode 100644 fas/fas/templates/groupList.kid rename fas/fas/templates/{home.kid => home.html} (75%) rename fas/fas/templates/{invite.kid => invite.html} (83%) create mode 100644 fas/fas/templates/login.html delete mode 100644 fas/fas/templates/login.kid rename fas/fas/templates/{master.kid => master.html} (78%) delete mode 100644 fas/fas/templates/resetPassword.kid delete mode 100644 fas/fas/templates/signUp.kid create mode 100644 fas/fas/templates/user/__init__.py create mode 100644 fas/fas/templates/user/__init__.pyc create mode 100644 fas/fas/templates/user/changepass.html create mode 100644 fas/fas/templates/user/edit.html rename fas/fas/templates/{userList.kid => user/list.html} (60%) create mode 100644 fas/fas/templates/user/new.html create mode 100644 fas/fas/templates/user/resetpass.html rename fas/fas/templates/{viewAccount.kid => user/view.html} (59%) delete mode 100644 fas/fas/templates/viewGroup.kid rename fas/fas/templates/{welcome.kid => welcome.html} (59%) create mode 100644 fas/fas/user.py diff --git a/fas/fas/auth.py b/fas/fas/auth.py new file mode 100644 index 0000000..fa59714 --- /dev/null +++ b/fas/fas/auth.py @@ -0,0 +1,52 @@ +from turbogears import config + +from fas.fasLDAP import UserAccount +from fas.fasLDAP import Person +from fas.fasLDAP import Groups +from fas.fasLDAP import UserGroup + +ADMINGROUP = config.get('admingroup') + +def isAdmin(userName, g=None): + if not g: + g = Groups.byUserName(userName) + try: + g[ADMINGROUP].cn + return True + except KeyError: + return False + +def canAdminGroup(userName, groupName, g=None): + # TODO: Allow the group owner to admin a group. + if not g: + g = Groups.byUserName(userName) + try: + if isAdmin(userName, g) or \ + (g[groupName].fedoraRoleType.lower() == 'administrator'): + return True + else: + return False + except: + return False + +def canSponsorGroup(userName, groupName, g=None): + if not g: + g = Groups.byUserName(userName) + try: + if isAdmin(userName, g) or \ + canAdminGroup(userName, groupName, g) or \ + (g[groupName].fedoraRoleType.lower() == 'sponsor'): + return True + else: + return False + except: + return False + +def canEditUser(userName, editUserName): + if userName == editUserName: + return True + elif isAdmin(userName): + return True + else: + return False + diff --git a/fas/fas/config/app.cfg b/fas/fas/config/app.cfg index 6e35357..e3a7147 100644 --- a/fas/fas/config/app.cfg +++ b/fas/fas/config/app.cfg @@ -13,6 +13,7 @@ admingroup = 'accounts' # which view (template engine) to use if one is not specified in the # template name # tg.defaultview = "kid" +tg.defaultview = "genshi" # The following kid settings determine the settings used by the kid serializer. diff --git a/fas/fas/controllers.py b/fas/fas/controllers.py index 9cebfd5..5209768 100644 --- a/fas/fas/controllers.py +++ b/fas/fas/controllers.py @@ -10,106 +10,34 @@ from turbogears import exception_handler import turbogears import ldap import time +from operator import itemgetter + +from fas.user import User +from fas.group import Group + # from fas import json # import logging # log = logging.getLogger("fas.controllers") -ADMINGROUP = config.get('admingroup') - -class knownUser(validators.FancyValidator): - def _to_python(self, value, state): - return value.strip() - def validate_python(self, value, state): - p = Person.byUserName(value) - if p.cn: - raise validators.Invalid(_("'%s' already exists") % value, value, state) - -class unknownUser(validators.FancyValidator): - def _to_python(self, value, state): - return value.strip() - def validate_python(self, value, state): - p = Person.byUserName(value) - if not p.cn: - raise validators.Invalid(_("'%s' does not exist") % value, value, state) - -class unknownGroup(validators.FancyValidator): - def _to_python(self, value, state): - return value.strip() - def validate_python(self, value, state): - g = Groups.groups(groupName) - if not g: - raise validators.Invalid(_("'%s' does not exist") % value, value, state) - - -class newPerson(widgets.WidgetsList): -# cn = widgets.TextField(label='Username', validator=validators.PlainText(not_empty=True, max=10)) - cn = widgets.TextField(label=_('Username'), validator=validators.All(knownUser(not_empty=True, max=10), validators.String(max=32, min=3))) - givenName = widgets.TextField(label=_('Full Name'), validator=validators.String(not_empty=True, max=42)) - mail = widgets.TextField(label=_('Email'), validator=validators.Email(not_empty=True, strip=True)) - telephoneNumber = widgets.TextField(label=_('Telephone Number'), validator=validators.PhoneNumber(not_empty=True)) - postalAddress = widgets.TextArea(label=_('Postal Address'), validator=validators.NotEmpty) - -newPersonForm = widgets.ListForm(fields=newPerson(), submit_text=_('Sign Up')) - -class editPerson(widgets.WidgetsList): -# cn = widgets.TextField(label='Username', validator=validators.PlainText(not_empty=True, max=10)) - userName = widgets.HiddenField(validator=validators.All(unknownUser(not_empty=True, max=10), validators.String(max=32, min=3))) - givenName = widgets.TextField(label=_('Full Name'), validator=validators.String(not_empty=True, max=42)) - mail = widgets.TextField(label=_('Email'), validator=validators.Email(not_empty=True, strip=True)) - fedoraPersonBugzillaMail = widgets.TextField(label=_('Bugzilla Email'), validator=validators.Email(not_empty=True, strip=True)) - fedoraPersonIrcNick = widgets.TextField(label=_('IRC Nick')) - fedoraPersonKeyId = widgets.TextField(label=_('PGP Key')) - telephoneNumber = widgets.TextField(label=_('Telephone Number'), validator=validators.PhoneNumber(not_empty=True)) - postalAddress = widgets.TextArea(label=_('Postal Address'), validator=validators.NotEmpty) - description = widgets.TextArea(label=_('Description')) - -editPersonForm = widgets.ListForm(fields=editPerson(), submit_text=_('Update')) - -class editGroup(widgets.WidgetsList): - groupName = widgets.HiddenField(validator=validators.All(unknownGroup(not_empty=True, max=10), validators.String(max=32, min=3))) - fedoraGroupDesc = widgets.TextField(label=_('Description'), validator=validators.NotEmpty) - fedoraGroupOwner = widgets.TextField(label=_('Group Owner'), validator=validators.All(knownUser(not_empty=True, max=10), validators.String(max=32, min=3))) - fedoraGroupNeedsSponsor = widgets.CheckBox(label=_('Needs Sponsor')) - fedoraGroupUserCanRemove = widgets.CheckBox(label=_('Self Removal')) - fedoraGroupJoinMsg = widgets.TextField(label=_('Group Join Message')) - -editGroupForm = widgets.ListForm(fields=editGroup(), submit_text=_('Update')) - -class findUser(widgets.WidgetsList): - userName = widgets.AutoCompleteField(label=_('Username'), search_controller='search', search_param='userName', result_name='people') - action = widgets.HiddenField(default='apply', validator=validators.String(not_empty=True)) - groupName = widgets.HiddenField(validator=validators.String(not_empty=True)) - -searchUserForm = widgets.ListForm(fields=findUser(), submit_text=_('Invite')) - - class Root(controllers.RootController): - @expose(template="fas.templates.error") - def errorMessage(self, tg_exceptions=None): - ''' Generic exception handler''' - # Maybe add a popup or alert or some damn thing. - message = '%s' % tg_exceptions - return dict(handling_value=True,exception=message) + + user = User() + group = Group() @expose(template="fas.templates.welcome") # @identity.require(identity.in_group("admin")) def index(self): - # log.debug("Happy TurboGears Controller Responding For Duty") if turbogears.identity.not_anonymous(): turbogears.redirect('home') return dict(now=time.ctime()) @expose(template="fas.templates.home") + @identity.require(identity.not_anonymous()) def home(self): from feeds import Koji builds = Koji(turbogears.identity.current.user_name) return dict(builds=builds) - @expose(template="fas.templates.dump", format="plain", content_type="text/plain") - def groupDump(self, groupName=None): - groups = Groups.byGroupName(groupName) - return dict(groups=groups, Person=Person) - @expose(template="fas.templates.login") def login(self, forward_url=None, previous_url=None, *args, **kw): @@ -143,408 +71,23 @@ class Root(controllers.RootController): turbogears.flash(_('You have successfully logged out.')) raise redirect("/") - @expose(template="fas.templates.viewAccount") - @identity.require(identity.not_anonymous()) - def viewAccount(self,userName=None, action=None): - if not userName: - userName = turbogears.identity.current.user_name - if turbogears.identity.current.user_name == userName: - personal = True - else: - personal = False - try: - Groups.byUserName(turbogears.identity.current.user_name)[ADMINGROUP].cn - admin = True - except KeyError: - admin = False - user = Person.byUserName(userName) - groups = Groups.byUserName(userName) - groupsPending = Groups.byUserName(userName, unapprovedOnly=True) - groupdata={} - for g in groups: - groupdata[g] = Groups.groups(g)[g] - for g in groupsPending: - groupdata[g] = Groups.groups(g)[g] - try: - groups['cla_done'] - claDone=True - except KeyError: - claDone=None - return dict(user=user, groups=groups, groupsPending=groupsPending, action=action, groupdata=groupdata, claDone=claDone, personal=personal, admin=admin) - - @expose(template="fas.templates.editAccount") - @identity.require(identity.not_anonymous()) - def editAccount(self, userName=None, action=None): - if userName: - try: - Groups.byUserName(turbogears.identity.current.user_name)[ADMINGROUP].cn - if not userName: - userName = turbogears.identity.current.user_name - except KeyError: - turbogears.flash(_('You cannot edit %s') % userName ) - userName = turbogears.identity.current.user_name - else: - userName = turbogears.identity.current.user_name - user = Person.byUserName(userName) - value = {'userName' : userName, - 'givenName' : user.givenName, - 'mail' : user.mail, - 'fedoraPersonBugzillaMail' : user.fedoraPersonBugzillaMail, - 'fedoraPersonIrcNick' : user.fedoraPersonIrcNick, - 'fedoraPersonKeyId' : user.fedoraPersonKeyId, - 'telephoneNumber' : user.telephoneNumber, - 'postalAddress' : user.postalAddress, - 'description' : user.description, } - return dict(form=editPersonForm, value=value) - - @expose(template="fas.templates.viewGroup") - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") - @identity.require(identity.not_anonymous()) - def viewGroup(self, groupName): - try: - groups = Groups.byGroupName(groupName, includeUnapproved=True) - except KeyError: - raise ValueError, _('Group: %s - Does not exist!') % groupName - try: - group = Groups.groups(groupName)[groupName] - except TypeError: - raise ValueError, _('Group: %s - Does not exist!') % groupName - userName = turbogears.identity.current.user_name - try: - myStatus = groups[userName].fedoraRoleStatus - except KeyError: - # Not in group - myStatus = 'Not a Member' # This has say 'Not a Member' - except TypeError: - groups = {} - try: - me = groups[userName] - except: - me = UserGroup() - #searchUserForm.groupName.display('group') - #findUser.groupName.display(value='fff') - value = {'groupName' : group.cn} - return dict(groups=groups, group=group, me=me, searchUserForm=searchUserForm, value=value) - - @expose(template="fas.templates.editGroup") - @identity.require(identity.not_anonymous()) - def editGroup(self, groupName, action=None): - userName = turbogears.identity.current.user_name - try: - Groups.byUserName(userName)[ADMINGROUP].cn - except KeyError: - try: - Groups.byUserName(userName)[groupName] - if Groups.byUserName(userName)[groupName].fedoraRoleType.lower() != 'administrator': - raise KeyError - except KeyError: - turbogears.flash(_('You cannot edit %s') % groupName) - turbogears.redirect('viewGroup?groupName=%s' % groupName) - group = Groups.groups(groupName)[groupName] - value = {'groupName' : groupName, - 'fedoraGroupOwner' : group.fedoraGroupOwner, - 'fedoraGroupType' : group.fedoraGroupType, - 'fedoraGroupNeedsSponsor' : (group.fedoraGroupNeedsSponsor.upper() == 'TRUE'), - 'fedoraGroupUserCanRemove' : (group.fedoraGroupUserCanRemove.upper() == 'TRUE'), - 'fedoraGroupJoinMsg' : group.fedoraGroupJoinMsg, - 'fedoraGroupDesc' : group.fedoraGroupDesc, } - #'fedoraGroupRequires' : group.fedoraGroupRequires, } - return dict(form=editGroupForm, value=value) - - @expose(template="fas.templates.groupList") - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") - @identity.require(identity.not_anonymous()) - def listGroup(self, search='*'): - groups = Groups.groups(search) - userName = turbogears.identity.current.user_name - myGroups = Groups.byUserName(userName) - try: - groups.keys() - except: - turbogears.flash(_("No Groups found matching '%s'") % search) - groups = {} - return dict(groups=groups, search=search, myGroups=myGroups) - - @expose(template="fas.templates.resetPassword") - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") - def resetPassword(self, userName=None, password=None, passwordCheck=None, mail=None): - import turbomail - - # Logged in - if turbogears.identity.current.user_name and not password: - return dict() - - # Not logged in - if not (userName and mail) and not turbogears.identity.current.user_name: -# turbogears.flash('Please provide your username and password') - return dict() - - if turbogears.identity.current.user_name: - userName = turbogears.identity.current.user_name - p = Person.byUserName(userName) - - if password and passwordCheck: - if not password == passwordCheck: - turbogears.flash(_('Passwords do not match!')) - return dict() - if len(password) < 8: - turbogears.flash(_('Password is too short. Must be at least 8 characters long')) - return dict() - newpass = p.generatePassword(password) - - if userName and mail and not turbogears.identity.current.user_name: - if not mail == p.mail: - turbogears.flash(_("username + email combo unknown.")) - return dict() - newpass = p.generatePassword() - message = turbomail.Message('accounts@fedoraproject.org', p.mail, _('Fedora Project Password Reset')) - message.plain = _("You have requested a password reset! Your new password is - %s \nPlease go to https://admin.fedoraproject.org/fas/ to change it") % newpass['pass'] - turbomail.enqueue(message) - p.__setattr__('userPassword', newpass['hash']) - - p.userPassword = newpass['hash'] - print "PASS: %s" % newpass['pass'] - - if turbogears.identity.current.user_name: - turbogears.flash(_("Password Changed")) - turbogears.redirect("viewAccount") - else: - turbogears.flash(_('Your password has been emailed to you')) - return dict() - - @expose(template="fas.templates.userList") - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") - @identity.require(identity.in_group("accounts")) - def listUser(self, search='a*'): - users = Person.users(search) - try: - users[0] - except: - turbogears.flash(_("No users found matching '%s'") % search) - users = [] - cla_done = Groups.byGroupName('cla_done') - claDone = {} - for u in users: - try: - cla_done[u] - claDone[u] = True - except KeyError: - claDone[u] = False - return dict(users=users, claDone=claDone, search=search) - - listUsers = listUser - -# @expose(template='fas.templates.apply') -# @exception_handler(errorMessage, rules="isinstance(tg_exceptions,ValueError)") -# @identity.require(identity.not_anonymous()) -# def sudo(self, userName): -# # This doesn't work -# turbogears.identity.current.user_name=userName -# turbogears.flash('Sudoed to %s' % userName) -# turbogears.recirect('viewAccount') - -# @error_handler(viewGroup) -# @validate(form=newPersonForm) - @expose(template='fas.templates.apply') - @identity.require(identity.not_anonymous()) - def modifyGroup(self, groupName, action, userName, **kw): - ''' Modifies group based on action, groupName and userName ''' - try: - userName = userName['text'] - except TypeError: - pass - - sponsor = turbogears.identity.current.user_name - try: - group = Groups.groups(groupName)[groupName] - except KeyError: - turbogears.flash(_('Group Error: %s does not exist.') % groupName) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - try: - p = Person.byUserName(userName) - if not p.cn: - raise KeyError, userName - except KeyError: - turbogears.flash(_('User Error: User %s does not exist.') % userName) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - - g = Groups.byGroupName(groupName, includeUnapproved=True) - - # Apply user to a group (as in application) - if action == 'apply': - try: - Groups.apply(groupName, userName) - except ldap.ALREADY_EXISTS: - turbogears.flash(_('%s Already in group!') % p.cn) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - else: - turbogears.flash(_('%s Applied!') % p.cn) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - - # Some error checking for the sponsors - if g[userName].fedoraRoleType.lower() == 'administrator' and g[sponsor].fedoraRoleType.lower() == 'sponsor': - raise ValueError, _('Sponsors cannot alter administrators. End of story.') - - try: - userGroup = Groups.byGroupName(groupName)[userName] - except KeyError: - # User not already in the group (happens when users apply for a group) - userGroup = UserGroup() - pass - - # Remove user from a group - if action == 'remove': - try: - Groups.remove(group.cn, p.cn) - except TypeError: - turbogears.flash(_('%(name)s could not be removed from %(group)s!') % {'name' : p.cn, 'group' : group.cn}) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - else: - turbogears.flash(_('%(name)s removed from %(group)s!') % {'name' : p.cn, 'group' : group.cn}) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - return dict() - - # Upgrade user in a group - elif action == 'upgrade': - if g[userName].fedoraRoleType.lower() == 'sponsor' and g[sponsor].fedoraRoleType.lower() == 'sponsor': - raise ValueError, _('Sponsors cannot admin other sponsors') - try: - p.upgrade(groupName) - except TypeError, e: - turbogears.flash(_('Cannot upgrade %(name)s - %(error)s!') % {'name' : p.cn, 'error' : e}) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - turbogears.flash(_('%s Upgraded!') % p.cn) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - - - # Downgrade user in a group - elif action == 'downgrade': - if g[userName].fedoraRoleType.lower() == 'administrator' and g[sponsor].fedoraRoleType.lower() == 'sponsor': - raise ValueError, _('Sponsors cannot downgrade admins') - try: - p.downgrade(groupName) - except TypeError, e: - turbogears.flash(_('Cannot downgrade %(name)s - %(error)s!') % {'name' : p.cn, 'error' : e}) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - turbogears.flash(_('%s Downgraded!') % p.cn) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - - # Sponsor / Approve User - elif action == 'sponsor' or action == 'apply': - p.sponsor(groupName, sponsor) - turbogears.flash(_('%s has been sponsored!') % p.cn) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - - turbogears.flash(_('Invalid action: %s') % action) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - return dict() - + ## TODO: Invitation cleanup- move out and validate! @expose(template='fas.templates.inviteMember') - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") @identity.require(identity.not_anonymous()) def inviteMember(self, name=None, email=None, skills=None): if name and email: - turbogears.flash(_('Invitation Sent to: "%(name)s" <%(email)s>') % {'name' : name, 'email' : email}) + turbogears.flash(_('Invitation Sent to: "%(name)s" <%(email)s>') % {'name': name, 'email': email}) if name or email:#FIXME turbogears.flash(_('Please provide both an email address and the persons name.')) return dict() - @expose(template='fas.templates.apply') - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") - @identity.require(identity.not_anonymous()) - def applyForGroup(self, groupName, action=None, requestField=None): - userName = turbogears.identity.current.user_name - - group = Groups.groups(groupName)[groupName] - user = Person.byUserName(userName) - if action != 'Remove': - try: - Groups.apply(groupName, userName) - turbogears.flash(_('Application sent for %s') % user.cn) - except ldap.ALREADY_EXISTS, e: - turbogears.flash(_('Application Denied: %s') % e[0]['desc']) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - - if action == 'Remove' and group.fedoraGroupUserCanRemove == 'TRUE': - try: - Groups.remove(group.cn, user.cn) - except TypeError: - turbogears.flash(_('%(user)s could not be removed from %(group)s!') % {'user' : user.cn, 'group' : group.cn}) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - else: - turbogears.flash(_('%(user)s removed from %(group)s!') % {'user' : user.cn, 'group' : group.cn}) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - else: - turbogears.flash(_('%s does not allow self removal') % group.cn) - turbogears.redirect('viewGroup?groupName=%s' % group.cn) - return dict() - - @expose(template='fas.templates.signUp') - def signUp(self): - if turbogears.identity.not_anonymous(): - turbogears.flash(_('No need to sign up, You have an account!')) - turbogears.redirect('viewAccount') - return dict(form=newPersonForm) - - @validate(form=newPersonForm) - @error_handler(signUp) - @expose(template='fas.templates.signUp') - def newAccountSubmit(self, cn, givenName, mail, telephoneNumber, postalAddress): - import turbomail - try: - Person.newPerson(cn.encode('utf8'), givenName.encode('utf8'), mail.encode('utf8'), telephoneNumber.encode('utf8'), postalAddress.encode('utf8')) - p = Person.byUserName(cn.encode('utf8')) - newpass = p.generatePassword() - message = turbomail.Message('accounts@fedoraproject.org', p.mail, _('Fedora Project Password Reset')) - message.plain = _("You have requested a password reset! Your new password is - %s \nPlease go to https://admin.fedoraproject.org/fas/ to change it") % newpass['pass'] - turbomail.enqueue(message) - p.__setattr__('userPassword', newpass['hash']) - turbogears.flash(_('Your password has been emailed to you. Please log in with it and change your password')) - turbogears.redirect('/') - - except ldap.ALREADY_EXISTS: - turbogears.flash(_('%s Already Exists, Please pick a different name') % cn) - turbogears.redirect('signUp') - return dict() - - @validate(form=editPersonForm) - @error_handler(editAccount) - @expose(template='fas.templates.editAccount') - def editAccountSubmit(self, givenName, mail, fedoraPersonBugzillaMail, telephoneNumber, postalAddress, userName=None, fedoraPersonIrcNick='', fedoraPersonKeyId='', description=''): - if userName: - try: - Groups.byUserName(turbogears.identity.current.user_name)[ADMINGROUP].cn - if not userName: - userName = turbogears.identity.current.user_name - except KeyError: - turbogears.flash(_('You cannot view %s') % userName) - userName = turbogears.identity.current.user_name - turbogears.redirect("editAccount") - return dict() - else: - userName = turbogears.identity.current.user_name - user = Person.byUserName(userName) - user.__setattr__('givenName', givenName.encode('utf8')) - user.__setattr__('mail', mail.encode('utf8')) - user.__setattr__('fedoraPersonBugzillaMail', fedoraPersonBugzillaMail.encode('utf8')) - user.__setattr__('fedoraPersonIrcNick', fedoraPersonIrcNick.encode('utf8')) - user.__setattr__('fedoraPersonKeyId', fedoraPersonKeyId.encode('utf8')) - user.__setattr__('telephoneNumber', telephoneNumber.encode('utf8')) - user.__setattr__('postalAddress', postalAddress.encode('utf8')) - user.__setattr__('description', description.encode('utf8')) - turbogears.flash(_('Your account has been updated.')) - turbogears.redirect("viewAccount?userName=%s" % userName) - return dict() - @expose(format="json") def search(self, userName=None, groupName=None): people = Person.users('%s*' % userName) return dict(people= filter(lambda item: userName in item.lower(), people)) - @expose(template='fas.templates.invite') - @exception_handler(errorMessage,rules="isinstance(tg_exceptions,ValueError)") @identity.require(identity.not_anonymous()) def invite(self, target=None): import turbomail @@ -566,7 +109,7 @@ place for you whether you're an artist, a web site builder, a writer, or \ a people person. You'll grow and learn as you work on a team with other \ very smart and talented people. \n\ \n\ -Fedora and FOSS are changing the world -- come be a part of it!") % {'name' : user.givenName, 'email' : user.mail} +Fedora and FOSS are changing the world -- come be a part of it!") % {'name': user.givenName, 'email': user.mail} turbomail.enqueue(message) turbogears.flash(_('Message sent to: %s') % target) return dict(target=target, user=user) @@ -574,4 +117,3 @@ Fedora and FOSS are changing the world -- come be a part of it!") % {'name' : us def relativeUser(realUser, sudoUser): ''' Takes user and sees if they are allow to sudo for remote group''' p = Person.byUserName('realUser') - diff --git a/fas/fas/group.py b/fas/fas/group.py new file mode 100644 index 0000000..f97b8fb --- /dev/null +++ b/fas/fas/group.py @@ -0,0 +1,280 @@ +import turbogears +from turbogears import controllers, expose, paginate, identity, redirect, widgets, validate, validators, error_handler + +import ldap + +from fas.fasLDAP import UserAccount +from fas.fasLDAP import Person +from fas.fasLDAP import Groups +from fas.fasLDAP import UserGroup + +from fas.auth import isAdmin, canAdminGroup, canSponsorGroup, canEditUser + +from operator import itemgetter + +from fas.user import knownUser + +class knownGroup(validators.FancyValidator): + def _to_python(self, value, state): + return value.strip() + def validate_python(self, value, state): + g = Groups.groups(groupName) + if g: + raise validators.Invalid(_("The group '%s' already exists") % value, value, state) + +class unknownGroup(validators.FancyValidator): + def _to_python(self, value, state): + return value.strip() + def validate_python(self, value, state): + g = Groups.groups(groupName) + if not g: + raise validators.Invalid(_("The group '%s' does not exist") % value, value, state) + +class createGroup(widgets.WidgetsList): + groupName = widgets.TextField(label=_('Group Name'), validator=validators.All(knownGroup(not_empty=True, max=10), validators.String(max=32, min=3))) + fedoraGroupDesc = widgets.TextField(label=_('Description'), validator=validators.NotEmpty) + fedoraGroupOwner = widgets.TextField(label=_('Group Owner'), validator=validators.All(knownUser(not_empty=True, max=10), validators.String(max=32, min=3))) + fedoraGroupNeedsSponsor = widgets.CheckBox(label=_('Needs Sponsor')) + fedoraGroupUserCanRemove = widgets.CheckBox(label=_('Self Removal')) + fedoraGroupJoinMsg = widgets.TextField(label=_('Group Join Message')) + +createGroupForm = widgets.ListForm(fields=createGroup(), submit_text=_('Create')) + +class editGroup(widgets.WidgetsList): + groupName = widgets.HiddenField(validator=validators.All(unknownGroup(not_empty=True, max=10), validators.String(max=32, min=3))) + fedoraGroupDesc = widgets.TextField(label=_('Description'), validator=validators.NotEmpty) + fedoraGroupOwner = widgets.TextField(label=_('Group Owner'), validator=validators.All(knownUser(not_empty=True, max=10), validators.String(max=32, min=3))) + fedoraGroupNeedsSponsor = widgets.CheckBox(label=_('Needs Sponsor')) + fedoraGroupUserCanRemove = widgets.CheckBox(label=_('Self Removal')) + fedoraGroupJoinMsg = widgets.TextField(label=_('Group Join Message')) + +editGroupForm = widgets.ListForm(fields=editGroup(), submit_text=_('Update')) + +class findUser(widgets.WidgetsList): + userName = widgets.AutoCompleteField(label=_('Username'), search_controller='search', search_param='userName', result_name='people') + action = widgets.HiddenField(default='apply', validator=validators.String(not_empty=True)) + groupName = widgets.HiddenField(validator=validators.String(not_empty=True)) + +findUserForm = widgets.ListForm(fields=findUser(), submit_text=_('Invite')) + +class Group(controllers.Controller): + + def __init__(self): + '''Create a Group Controller.''' + + def index(self): + '''Perhaps show a nice explanatory message about groups here?''' + return dict() + + @expose(template="fas.templates.group.view") + @identity.require(turbogears.identity.not_anonymous()) + def view(self, groupName): + '''View group''' + # FIXME: Cleaner checks + try: + groups = Groups.byGroupName(groupName, includeUnapproved=True) + except KeyError: + raise ValueError, _('Group: %s - Does not exist!') % groupName + try: + group = Groups.groups(groupName)[groupName] + except TypeError: + raise ValueError, _('Group: %s - Does not exist!') % groupName + userName = turbogears.identity.current.user_name + try: + myStatus = groups[userName].fedoraRoleStatus + except KeyError: + # Not in group + myStatus = 'Not a Member' # This has say 'Not a Member' + except TypeError: + groups = {} + try: + me = groups[userName] + except: + me = UserGroup() + #searchUserForm.groupName.display('group') + #findUser.groupName.display(value='fff') + value = {'groupName': group.cn} + groups = sorted(groups.items(), key=itemgetter(0)) + return dict(userName=userName, groups=groups, group=group, me=me, value=value) + + @expose(template="fas.templates.group.new") + @identity.require(turbogears.identity.not_anonymous()) + def new(self, groupName): + '''Create a group''' + return dict() + + #@validate(form=createGroupForm) + @expose(template="fas.templates.group.new") + @identity.require(turbogears.identity.not_anonymous()) + def create(self, groupName, fedoraGroupDesc, fedoraGroupOwner, fedoraGroupNeedsSponsor=True, fedoraGroupUserCanRemove=True, fedoraGroupJoinMsg=""): + userName = turbogears.identity.current.user_name + if not isAdmin(userName): + turbogears.flash(_('Only FAS adminstrators can create groups.')) + # TODO: Create a general access denied/error page. + turbogears.redirect('/') + try: + Groups.newGroup(groupName, fedoraGroupDesc, fedoraGroupOwner, fedoraGroupNeedsSponsor, fedoraGroupUserCanRemove, fedoraGroupJoinMsg) + turbogears.flash(_("The group: '%s' has been created.") % groupName) + turbogears.redirect('/group/view/%s', groupName) + except: + turbogears.flash(_("The group: '%s' could not be created.") % groupName) + return dict() + + @expose(template="fas.templates.group.edit") + @identity.require(turbogears.identity.not_anonymous()) + def edit(self, groupName): + '''Edit a group''' + #TODO: Handle the "no such group" case (or maybe create + #a generic function to check user/group existence. + userName = turbogears.identity.current.user_name + if not canAdminGroup(userName, groupName): + turbogears.flash(_('You cannot edit %s') % groupName) + turbogears.redirect('/group/view/%s' % groupName) + group = Groups.groups(groupName)[groupName] + value = {'groupName': groupName, + 'fedoraGroupOwner': group.fedoraGroupOwner, + 'fedoraGroupType': group.fedoraGroupType, + 'fedoraGroupNeedsSponsor': (group.fedoraGroupNeedsSponsor.upper() == 'TRUE'), + 'fedoraGroupUserCanRemove': (group.fedoraGroupUserCanRemove.upper() == 'TRUE'), + 'fedoraGroupJoinMsg': group.fedoraGroupJoinMsg, + 'fedoraGroupDesc': group.fedoraGroupDesc, } + #'fedoraGroupRequires': group.fedoraGroupRequires, } + return dict(value=value) + + #@validate(form=editGroupForm) + @expose(template="fas.templates.group.edit") + @identity.require(turbogears.identity.not_anonymous()) + def save(self, stuff): + #TODO + return dict() + + @expose(template="fas.templates.group.list") + @identity.require(turbogears.identity.not_anonymous()) + def list(self, search='*'): + groups = Groups.groups(search) + userName = turbogears.identity.current.user_name + myGroups = Groups.byUserName(userName) + try: + groups.keys() + except: + turbogears.flash(_("No Groups found matching '%s'") % search) + groups = {} + groups = sorted(groups.items(), key=itemgetter(0)) + return dict(groups=groups, search=search, myGroups=myGroups) + + # TODO: Validate + @expose(template='fas.templates.group.view') + @identity.require(turbogears.identity.not_anonymous()) + def apply(self, groupName, userName): + try: + Groups.apply(groupName, userName) + except ldap.ALREADY_EXISTS: + turbogears.flash(_('%(user)s is already in %(group)s!') % {'user': userName, 'group': groupName}) + turbogears.redirect('/group/view/%s' % groupName) + else: + turbogears.flash(_('%(user)s has applied to %(group)s!') % {'user': userName, 'group': groupName}) + turbogears.redirect('/group/view/%s' % group.cn) + + # TODO: Validate (user doesn't exist case) + @expose(template='fas.templates.group.view') + @identity.require(turbogears.identity.not_anonymous()) + def sponsor(self, groupName, userName): + '''Sponsor user''' + sponsor = turbogears.identity.current.user_name + if not canSponsorGroup(sponsor, groupName): + turbogears.flash(_("You are not a sponsor for '%s'") % groupName) + turbogears.redirect('/group/view/%s' % groupName) + try: + group = Groups.groups(groupName)[groupName] + except KeyError: + turbogears.flash(_('Group Error: %s does not exist.') % groupName) + # The following line is kind of pointless- any suggestions? + turbogears.redirect('/group/view/%s' % groupName) + p = Person.byUserName(userName) + g = Groups.byGroupName(groupName, includeUnapproved=True) + # TODO: Check if the person actually applied to the group. + p.sponsor(groupName, sponsor) + turbogears.flash(_('%s has been sponsored!') % p.cn) + turbogears.redirect('/group/view/%s' % groupName) + + # TODO: Validate (user doesn't exist case) + @expose(template='fas.templates.group.view') + @identity.require(turbogears.identity.not_anonymous()) + def remove(self, groupName, userName): + '''Remove user from group''' + # TODO: Add confirmation? + sponsor = turbogears.identity.current.user_name + if not canSponsorGroup(sponsor, groupName) \ + and sponsor != userName: # Users can remove themselves + turbogears.flash(_("You are not a sponsor for '%s'") % groupName) + turbogears.redirect('/group/view/%s' % groupName) + if canAdminGroup(userName, groupName) \ + and (not canAdminGroup(sponsor, groupName)): + turbogears.flash(_('Sponsors cannot remove administrators.') % userName) + turbogears.redirect('/group/view/%s' % groupName) + try: + Groups.remove(groupName, userName) + except TypeError: + turbogears.flash(_('%(name)s could not be removed from %(group)s!') % {'name': userName, 'group': groupName}) + turbogears.redirect('/group/view/%s' % groupName) + else: + turbogears.flash(_('%(name)s has been removed from %(group)s!') % {'name': userName, 'group': groupName}) + turbogears.redirect('/group/view/%s' % groupName) + return dict() + + # TODO: Validate (user doesn't exist case) + @expose(template='fas.templates.group.view') + @identity.require(turbogears.identity.not_anonymous()) + def upgrade(self, groupName, userName): + '''Upgrade user in group''' + sponsor = turbogears.identity.current.user_name + if not canSponsorGroup(sponsor, groupName): + turbogears.flash(_("You are not a sponsor for '%s'") % groupName) + turbogears.redirect('/group/view/%s' % groupName) + # This is already checked in fasLDAP.py + #if canAdminGroup(userName, groupName): + # turbogears.flash(_('Group administators cannot be upgraded any further.')) + # turbogears.redirect('/group/view/%s' % groupName) + elif canSponsorGroup(userName, groupName) \ + and (not canAdminGroup(sponsor, groupName)): + turbogears.flash(_('Sponsors cannot upgrade other sponsors.') % userName) + turbogears.redirect('/group/view/%s' % groupName) + p = Person.byUserName(userName) + try: + p.upgrade(groupName) + except: + turbogears.flash(_('%(name)s could not be upgraded!') % userName) + turbogears.redirect('/group/view/%s' % groupName) + turbogears.flash(_('%s has been upgraded!') % userName) + turbogears.redirect('/group/view/%s' % groupName) + + # TODO: Validate (user doesn't exist case) + @expose(template='fas.templates.group.view') + @identity.require(turbogears.identity.not_anonymous()) + def downgrade(self, groupName, userName): + '''Upgrade user in group''' + sponsor = turbogears.identity.current.user_name + if not canSponsorGroup(sponsor, groupName): + turbogears.flash(_("You are not a sponsor for '%s'") % groupName) + turbogears.redirect('/group/view/%s' % groupName) + if canAdminGroup(userName, groupName) \ + and (not canAdminGroup(sponsor, groupName)): + turbogears.flash(_('Sponsors cannot downgrade group administrators.') % userName) + turbogears.redirect('/group/view/%s' % groupName) + p = Person.byUserName(userName) + try: + p.upgrade(groupName) + except: + turbogears.flash(_('%(name)s could not be downgraded!') % userName) + turbogears.redirect('/group/view/%s' % groupName) + turbogears.flash(_('%s has been downgraded!') % p.cn) + turbogears.redirect('/group/view/%s' % groupName) + + # TODO: Validate (group doesn't exist case) + @expose(template="genshi-text:fas.templates.group.dump", content_type='text/plain; charset=utf-8') + @identity.require(turbogears.identity.not_anonymous()) + def dump(self, groupName=None): + groups = Groups.byGroupName(groupName) + groups = sorted(groups.items(), key=itemgetter(0)) + return dict(groups=groups, Person=Person) + diff --git a/fas/fas/static/css/style.css b/fas/fas/static/css/style.css index 42ec9fc..3ad8559 100644 --- a/fas/fas/static/css/style.css +++ b/fas/fas/static/css/style.css @@ -407,30 +407,29 @@ pre font-size: 3ex; } -form ul +form { list-style: none; margin: 1ex 0!important; } -form ul li +form .field { margin: 0 0 1ex; text-align: left; overflow: hidden; } -form ul label +form .field label { float: left; clear: left; width: 16ex; text-align: right; - margin: 0; padding: 0 2ex 0 0; } -form ul input, form ul textarea +form .field input, form .field textarea { margin: 0; } diff --git a/fas/fas/templates/autoComplete.kid b/fas/fas/templates/autoComplete.kid deleted file mode 100644 index 7fe9f4a..0000000 --- a/fas/fas/templates/autoComplete.kid +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/fas/fas/templates/dump.kid b/fas/fas/templates/dump.kid deleted file mode 100644 index 26a1308..0000000 --- a/fas/fas/templates/dump.kid +++ /dev/null @@ -1,7 +0,0 @@ - - - -
${user},${Person.byUserName(user).mail},${Person.byUserName(user).givenName},${groups[user].fedoraRoleType},0 -
- - diff --git a/fas/fas/templates/editAccount.kid b/fas/fas/templates/editAccount.kid deleted file mode 100644 index a5db8aa..0000000 --- a/fas/fas/templates/editAccount.kid +++ /dev/null @@ -1,10 +0,0 @@ - - - - Edit Account - - -

Edit Account

- ${form(action='editAccountSubmit', method='post', value=value)} - - diff --git a/fas/fas/templates/editGroup.kid b/fas/fas/templates/editGroup.kid deleted file mode 100644 index aff7bdc..0000000 --- a/fas/fas/templates/editGroup.kid +++ /dev/null @@ -1,10 +0,0 @@ - - - - Edit Group - - -

Edit Group

- ${form(action='editGroupSubmit', method='post', value=value)} - - diff --git a/fas/fas/templates/error.kid b/fas/fas/templates/error.kid deleted file mode 100644 index 32b89a7..0000000 --- a/fas/fas/templates/error.kid +++ /dev/null @@ -1,9 +0,0 @@ - - - - Crap! - - - ${exception} - - diff --git a/fas/fas/templates/group/__init__.py b/fas/fas/templates/group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fas/fas/templates/group/__init__.pyc b/fas/fas/templates/group/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c33caa2539c210f761e84e0bedf9e398fb3033c GIT binary patch literal 156 zcmd1(#LLB9f7mUV0SXuy7#JKF7#NCK7#J8*7#M;zKq7t`AZawB#D{@_K|douH&s6^ zH6_0&QNJiNIlB_d(ap?DD@rUbDJo4aDJ@FXPfIKYm?Ha literal 0 HcmV?d00001 diff --git a/fas/fas/templates/group/dump.txt b/fas/fas/templates/group/dump.txt new file mode 100644 index 0000000..5fab998 --- /dev/null +++ b/fas/fas/templates/group/dump.txt @@ -0,0 +1,3 @@ +#for user in groups +${user[0].decode('utf-8')},${Person.byUserName(user[0]).mail.decode('utf-8')},${Person.byUserName(user[0]).givenName.decode('utf-8')},${user[1].fedoraRoleType.decode('utf-8')} +#end diff --git a/fas/fas/templates/group/edit.html b/fas/fas/templates/group/edit.html new file mode 100644 index 0000000..42c50f1 --- /dev/null +++ b/fas/fas/templates/group/edit.html @@ -0,0 +1,39 @@ + + + + + Edit Group + + +

Edit Group

+
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ +
+
+ + diff --git a/fas/fas/templates/group/list.html b/fas/fas/templates/group/list.html new file mode 100644 index 0000000..7e424c6 --- /dev/null +++ b/fas/fas/templates/group/list.html @@ -0,0 +1,44 @@ + + + + + Groups List + + +

List (${search})

+

Search Groups

+
+

"*" is a wildcard (Ex: "cvs*")

+
+ + +
+
+

Results

+ + + + + + + + + + + + + +
GroupDescriptionStatus
${group[1].cn}${group[1].fedoraGroupDesc} + + Approved + Unapproved + + Not a Member +
+ + diff --git a/fas/fas/templates/group/new.html b/fas/fas/templates/group/new.html new file mode 100644 index 0000000..9e7b04c --- /dev/null +++ b/fas/fas/templates/group/new.html @@ -0,0 +1,41 @@ + + + + + Create a new FAS Group + + +

Create a new FAS Group

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + diff --git a/fas/fas/templates/group/view.html b/fas/fas/templates/group/view.html new file mode 100644 index 0000000..05ca365 --- /dev/null +++ b/fas/fas/templates/group/view.html @@ -0,0 +1,87 @@ + + + + + Edit Group + + +

${group.fedoraGroupDesc} (${group.cn})

+

+ My Status: + Approved + Unapproved + Not a Member +

+
+
+ + +
+
+ Remove me +

Group Details (edit)

+
+
+
Name:
${group.cn}
+
Description:
${group.fedoraGroupDesc}
+
Owner:
${group.fedoraGroupOwner}
+
Type:
${group.fedoraGroupType}
+
Needs Sponsor:
+ Yes + No +
+
Self Removal
+ Yes + No +
+
Join Message:
${group.fedoraGroupJoinMsg}
+
+
+ +

Members

+ + + + + + + + + + + + + + + + + + + + + + + + + +
UsernameSponsorDate AddedDate ApprovedApprovalRole TypeAction
${user[0]}${user[1].fedoraRoleSponsor}${user[1].fedoraRoleSponsor}${user[1].fedoraRoleCreationDate}${user[1].fedoraRoleApprovalDate}${user[1].fedoraRoleStatus}${user[1].fedoraRoleType} + Sponsor + Approve + Delete + Upgrade + Downgrade Suspend + + Sponsor + Approve + Delete + Upgrade + Downgrade Suspend +
+ + diff --git a/fas/fas/templates/groupList.kid b/fas/fas/templates/groupList.kid deleted file mode 100644 index c7a6b45..0000000 --- a/fas/fas/templates/groupList.kid +++ /dev/null @@ -1,46 +0,0 @@ - - - - Groups List - - -

List (${search})

-

Search Groups

-
-

"*" is a wildcard (Ex: "cvs*")

-
- - -
-
-

Results

- - - - - - - - - - - - - - -
GroupDescriptionStatus
${group.cn}${group.fedoraGroupDesc} - - Approved - Unapproved - - Not a Member -
- - diff --git a/fas/fas/templates/home.kid b/fas/fas/templates/home.html similarity index 75% rename from fas/fas/templates/home.kid rename to fas/fas/templates/home.html index 1548cdf..ac00ab2 100644 --- a/fas/fas/templates/home.kid +++ b/fas/fas/templates/home.html @@ -1,5 +1,8 @@ - - + + + Fedora Accounts System @@ -18,6 +21,5 @@ ${builds.builds[build]['pubDate']} - diff --git a/fas/fas/templates/invite.kid b/fas/fas/templates/invite.html similarity index 83% rename from fas/fas/templates/invite.kid rename to fas/fas/templates/invite.html index 515454a..9e2c1ad 100644 --- a/fas/fas/templates/invite.kid +++ b/fas/fas/templates/invite.html @@ -1,11 +1,14 @@ - - + + + Invite a new community member!

Invite a new community member!

-
+
To email:
From: ${user.mail}
diff --git a/fas/fas/templates/login.html b/fas/fas/templates/login.html new file mode 100644 index 0000000..3cb846f --- /dev/null +++ b/fas/fas/templates/login.html @@ -0,0 +1,33 @@ + + + + + Login to the Fedora Accounts System + + + +

Login

+

${message}

+ +
+
+
+ + + +
+ + + + diff --git a/fas/fas/templates/login.kid b/fas/fas/templates/login.kid deleted file mode 100644 index 46607ee..0000000 --- a/fas/fas/templates/login.kid +++ /dev/null @@ -1,33 +0,0 @@ - - - - Login to the Fedora Accounts System - - - -

Login

-

${message}

-
-
    -
  • -
  • -
  • - - - -
  • -
-
- - - diff --git a/fas/fas/templates/master.kid b/fas/fas/templates/master.html similarity index 78% rename from fas/fas/templates/master.kid rename to fas/fas/templates/master.html index 1c56f54..b3c1598 100644 --- a/fas/fas/templates/master.kid +++ b/fas/fas/templates/master.html @@ -1,12 +1,13 @@ - - - - - Title + + + + ${title} - - +
@@ -45,17 +46,17 @@
${tg_flash}
-
+