From: stefan Date: Tue, 12 Oct 2010 01:14:07 +0000 (+0000) Subject: Add new tracker template. X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=44c7a50f09a975ca5736f7eb407f5b8b6ee317aa;p=roundup.git Add new tracker template. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4543 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/share/roundup/templates/devel/TEMPLATE-INFO.txt b/share/roundup/templates/devel/TEMPLATE-INFO.txt new file mode 100644 index 0000000..e17f808 --- /dev/null +++ b/share/roundup/templates/devel/TEMPLATE-INFO.txt @@ -0,0 +1,7 @@ +Name: devel +Description: This is a generic issue tracker that may be used to track bugs, + feature requests, project issues or any number of other types + of issues. Most users of Roundup will find that this template + suits them, with perhaps a few customisations. +Intended-For: Developers + diff --git a/share/roundup/templates/devel/config.ini b/share/roundup/templates/devel/config.ini new file mode 100644 index 0000000..ec78028 --- /dev/null +++ b/share/roundup/templates/devel/config.ini @@ -0,0 +1,383 @@ +# Roundup issue tracker configuration file +# Autogenerated at Fri Nov 17 16:59:49 2006 + +# WARNING! Following options need adjustments: +# [mail]: domain, host +# [tracker]: web + +[main] + +# Database directory path. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: db +database = db + +# Path to the HTML templates directory. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: html +templates = html + +# Path to directory holding additional static files +# available via Web UI. This directory may contain +# sitewide images, CSS stylesheets etc. and is searched +# for these files prior to the TEMPLATES directory +# specified above. If this option is not set, all static +# files are taken from the TEMPLATES directory +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: +static_files = + +# Email address that roundup will complain to if it runs into trouble. +# Default: roundup-admin +admin_email = roundup-admin + +# The 'dispatcher' is a role that can get notified +# of new items to the database. +# It is used by the ERROR_MESSAGES_TO config setting. +# Default: roundup-admin +dispatcher_email = roundup-admin + +# Additional text to include in the "name" part +# of the From: address used in nosy messages. +# If the sending user is "Foo Bar", the From: line +# is usually: "Foo Bar" +# the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so: +# "Foo Bar EMAIL_FROM_TAG" +# Default: +email_from_tag = + +# Roles that a user gets when they register with Web User Interface. +# This is a comma-separated string of role names (e.g. 'Admin,User'). +# Default: User +new_web_user_roles = User + +# Roles that a user gets when they register with Email Gateway. +# This is a comma-separated string of role names (e.g. 'Admin,User'). +# Default: User +new_email_user_roles = User + +# Send error message emails to the dispatcher, user, or both? +# The dispatcher is configured using the DISPATCHER_EMAIL setting. +# Default: user +error_messages_to = user + +# HTML version to generate. The templates are html4 by default. +# If you wish to make them xhtml, then you'll need to change this +# var to 'xhtml' too so all auto-generated HTML is compliant. +# Allowed values: html4, xhtml +# Default: html4 +html_version = xhtml + +# Default timezone offset, applied when user's timezone is not set. +# If pytz module is installed, value may be any valid +# timezone specification (e.g. EET or Europe/Warsaw). +# If pytz is not installed, value must be integer number +# giving local timezone offset from UTC in hours. +# Default: UTC +timezone = UTC + +# Register new users instantly, or require confirmation via +# email? +# Allowed values: yes, no +# Default: no +instant_registration = no + +# Offer registration confirmation by email or only through the web? +# Allowed values: yes, no +# Default: yes +email_registration_confirmation = yes + +# Additional stop-words for the full-text indexer specific to +# your tracker. See the indexer source for the default list of +# stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...) +# Allowed values: comma-separated list of words +# Default: +indexer_stopwords = + +# Defines the file creation mode mask. +# Default: 02 +umask = 02 + +[tracker] + +# A descriptive name for your roundup instance. +# Default: Roundup issue tracker +name = Roundup tracker + +# The web address that the tracker is viewable at. +# This will be included in information sent to users of the tracker. +# The URL MUST include the cgi-bin part or anything else +# that is required to get to the home page of the tracker. +# You MUST include a trailing '/' in the URL. +# Default: NO DEFAULT +web = + +# Email address that mail to roundup should go to. +# Default: issue_tracker +email = issue_tracker + +# Default locale name for this tracker. +# If this option is not set, the language is determined +# by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES, +# or LANG, in that order of preference. +# Default: +language = + +[web] + +# Whether to use HTTP Basic Authentication, if present. +# Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION +# variables supplied by your web server (in that order). +# Set this option to 'no' if you do not wish to use HTTP Basic +# Authentication in your web interface. +# Allowed values: yes, no +# Default: yes +http_auth = yes + +# Whether to use HTTP Accept-Language, if present. +# Browsers send a language-region preference list. +# It's usually set in the client's browser or in their +# Operating System. +# Set this option to 'no' if you want to ignore it. +# Allowed values: yes, no +# Default: yes +use_browser_language = no + +# Setting this option makes Roundup display error tracebacks +# in the user's browser rather than emailing them to the +# tracker admin. +# Allowed values: yes, no +# Default: no +debug = no + +# Settings in this section are used by Postgresql and MySQL backends only +[rdbms] + +# Name of the database to use. +# Default: roundup +name = roundup_roundup_tracker + +# Database server host. +# Default: localhost +host = localhost + +# TCP port number of the database server. +# Postgresql usually resides on port 5432 (if any), +# for MySQL default port number is 3306. +# Leave this option empty to use backend default +# Default: +port = + +# Database user name that Roundup should use. +# Default: roundup +user = roundup + +# Database user password. +# Default: roundup +password = roundup + +# Name of the MySQL defaults file. +# Only used in MySQL connections. +# Default: ~/.my.cnf +read_default_file = ~/.my.cnf + +# Name of the group to use in the MySQL defaults file (.my.cnf). +# Only used in MySQL connections. +# Default: roundup +read_default_group = roundup + +[logging] + +# Path to configuration file for standard Python logging module. +# If this option is set, logging configuration is loaded +# from specified file; options 'filename' and 'level' +# in this section are ignored. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: +config = + +# Log file name for minimal logging facility built into Roundup. +# If no file name specified, log messages are written on stderr. +# If above 'config' option is set, this option has no effect. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: +filename = + +# Minimal severity level of messages written to log file. +# If above 'config' option is set, this option has no effect. +# Allowed values: DEBUG, INFO, WARNING, ERROR +# Default: ERROR +level = ERROR + +# Outgoing email options. +# Used for nozy messages and approval requests +[mail] + +# Domain name used for email addresses. +# Default: NO DEFAULT +#domain = NO DEFAULT +domain = psf.upfronthosting.co.za + +# SMTP mail host that roundup will use to send mail +# Default: NO DEFAULT +#host = NO DEFAULT +host = localhost + +# SMTP login name. +# Set this if your mail host requires authenticated access. +# If username is not empty, password (below) MUST be set! +# Default: +username = + +# SMTP login password. +# Set this if your mail host requires authenticated access. +# Default: NO DEFAULT +#password = NO DEFAULT + +# If your SMTP mail host provides or requires TLS +# (Transport Layer Security) then set this option to 'yes'. +# Allowed values: yes, no +# Default: no +tls = no + +# If TLS is used, you may set this option to the name +# of a PEM formatted file that contains your private key. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: +tls_keyfile = + +# If TLS is used, you may set this option to the name +# of a PEM formatted certificate chain file. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: +tls_certfile = + +# Character set to encode email headers with. +# We use utf-8 by default, as it's the most flexible. +# Some mail readers (eg. Eudora) can't cope with that, +# so you might need to specify a more limited character set +# (eg. iso-8859-1). +# Default: utf-8 +charset = utf-8 + +# Setting this option makes Roundup to write all outgoing email +# messages to this file *instead* of sending them. +# This option has the same effect as environment variable SENDMAILDEBUG. +# Environment variable takes precedence. +# The path may be either absolute or relative +# to the directory containig this config file. +# Default: +#debug = /home/roundup/outgoing-mail +debug = + +# Roundup Mail Gateway options +[mailgw] + +# Keep email citations when accepting messages. +# Setting this to "no" strips out "quoted" text from the message. +# Signatures are also stripped. +# Allowed values: yes, no +# Default: yes +keep_quoted_text = yes + +# Preserve the email body as is - that is, +# keep the citations _and_ signatures. +# Allowed values: yes, no +# Default: no +leave_body_unchanged = no + +# Default class to use in the mailgw +# if one isn't supplied in email subjects. +# To disable, leave the value blank. +# Default: issue +default_class = issue + +# Default locale name for the tracker mail gateway. +# If this option is not set, mail gateway will use +# the language of the tracker instance. +# Default: +language = + +# Controls the parsing of the [prefix] on subject +# lines in incoming emails. "strict" will return an +# error to the sender if the [prefix] is not recognised. +# "loose" will attempt to parse the [prefix] but just +# pass it through as part of the issue title if not +# recognised. "none" will always pass any [prefix] +# through as part of the issue title. +# Default: strict +subject_prefix_parsing = strict + +# Controls the parsing of the [suffix] on subject +# lines in incoming emails. "strict" will return an +# error to the sender if the [suffix] is not recognised. +# "loose" will attempt to parse the [suffix] but just +# pass it through as part of the issue title if not +# recognised. "none" will always pass any [suffix] +# through as part of the issue title. +# Default: strict +subject_suffix_parsing = strict + +# Defines the brackets used for delimiting the prefix and +# suffix in a subject line. The presence of "suffix" in +# the config option name is a historical artifact and may +# be ignored. +# Default: [] +subject_suffix_delimiters = [] + +# Controls matching of the incoming email subject line +# against issue titles in the case where there is no +# designator [prefix]. "never" turns off matching. +# "creation + interval" or "activity + interval" +# will match an issue for the interval after the issue's +# creation or last activity. The interval is a standard +# Roundup interval. +# Default: always +subject_content_match = always + +# Nosy messages sending +[nosy] + +# Send nosy messages to the author of the message. +# Allowed values: yes, no, new +# Default: no +messages_to_author = yes + +# Where to place the email signature. +# Allowed values: top, bottom, none +# Default: bottom +signature_position = bottom + +# Does the author of a message get placed on the nosy list +# automatically? If 'new' is used, then the author will +# only be added when a message creates a new issue. +# If 'yes', then the author will be added on followups too. +# If 'no', they're never added to the nosy. +# +# Allowed values: yes, no, new +# Default: new +add_author = yes + +# Do the recipients (To:, Cc:) of a message get placed on the +# nosy list? If 'new' is used, then the recipients will +# only be added when a message creates a new issue. +# If 'yes', then the recipients will be added on followups too. +# If 'no', they're never added to the nosy. +# +# Allowed values: yes, no, new +# Default: new +add_recipients = new + +# Controls the email sending from the nosy reactor. If +# "multiple" then a separate email is sent to each +# recipient. If "single" then a single email is sent with +# each recipient as a CC address. +# Default: single +email_sending = multiple diff --git a/share/roundup/templates/devel/detectors/messagesummary.py b/share/roundup/templates/devel/detectors/messagesummary.py new file mode 100644 index 0000000..8d0349b --- /dev/null +++ b/share/roundup/templates/devel/detectors/messagesummary.py @@ -0,0 +1,20 @@ +#$Id: messagesummary.py,v 1.1 2009/02/01 22:23:25 stefan Exp $ + +from roundup.mailgw import parseContent + +def summarygenerator(db, cl, nodeid, newvalues): + ''' If the message doesn't have a summary, make one for it. + ''' + if newvalues.has_key('summary') or not newvalues.has_key('content'): + return + + summary, content = parseContent(newvalues['content'], config=db.config) + newvalues['summary'] = summary + + +def init(db): + # fire before changes are made + db.msg.audit('create', summarygenerator) + +# vim: set filetype=python ts=4 sw=4 et si +#SHA: 538f90cb7f4eb63f77eca252b87afbe037d29c48 diff --git a/share/roundup/templates/devel/detectors/no_texthtml.py b/share/roundup/templates/devel/detectors/no_texthtml.py new file mode 100644 index 0000000..cf47655 --- /dev/null +++ b/share/roundup/templates/devel/detectors/no_texthtml.py @@ -0,0 +1,9 @@ + +def audit_html_files(db, cl, nodeid, newvalues): + if newvalues.has_key('type') and newvalues['type'] == 'text/html': + newvalues['type'] = 'text/plain' + + +def init(db): + db.file.audit('set', audit_html_files) + db.file.audit('create', audit_html_files) diff --git a/share/roundup/templates/devel/detectors/nosyreaction.py b/share/roundup/templates/devel/detectors/nosyreaction.py new file mode 100644 index 0000000..67380a0 --- /dev/null +++ b/share/roundup/templates/devel/detectors/nosyreaction.py @@ -0,0 +1,122 @@ +import sets + +from roundup import roundupdb, hyperdb + +def nosyreaction(db, cl, nodeid, oldvalues): + ''' A standard detector is provided that watches for additions to the + "messages" property. + + When a new message is added, the detector sends it to all the users on + the "nosy" list for the issue that are not already on the "recipients" + list of the message. + + Those users are then appended to the "recipients" property on the + message, so multiple copies of a message are never sent to the same + user. + + The journal recorded by the hyperdatabase on the "recipients" property + then provides a log of when the message was sent to whom. + ''' + # send a copy of all new messages to the nosy list + for msgid in determineNewMessages(cl, nodeid, oldvalues): + try: + cl.nosymessage(nodeid, msgid, oldvalues) + except roundupdb.MessageSendError, message: + raise roundupdb.DetectorError, message + +def determineNewMessages(cl, nodeid, oldvalues): + ''' Figure a list of the messages that are being added to the given + node in this transaction. + ''' + messages = [] + if oldvalues is None: + # the action was a create, so use all the messages in the create + messages = cl.get(nodeid, 'messages') + elif oldvalues.has_key('messages'): + # the action was a set (so adding new messages to an existing issue) + m = {} + for msgid in oldvalues['messages']: + m[msgid] = 1 + messages = [] + # figure which of the messages now on the issue weren't there before + for msgid in cl.get(nodeid, 'messages'): + if not m.has_key(msgid): + messages.append(msgid) + return messages + +def updatenosy(db, cl, nodeid, newvalues): + '''Update the nosy list for changes to the assignedto + ''' + # nodeid will be None if this is a new node + current_nosy = sets.Set() + if nodeid is None: + ok = ('new', 'yes') + else: + ok = ('yes',) + # old node, get the current values from the node if they haven't + # changed + if not newvalues.has_key('nosy'): + nosy = cl.get(nodeid, 'nosy') + for value in nosy: + current_nosy.add(value) + + # if the nosy list changed in this transaction, init from the new value + if newvalues.has_key('nosy'): + nosy = newvalues.get('nosy', []) + for value in nosy: + if not db.hasnode('user', value): + continue + current_nosy.add(value) + + new_nosy = sets.Set(current_nosy) + + # add assignedto(s) to the nosy list + if newvalues.has_key('assignedto') and newvalues['assignedto'] is not None: + propdef = cl.getprops() + if isinstance(propdef['assignedto'], hyperdb.Link): + assignedto_ids = [newvalues['assignedto']] + elif isinstance(propdef['assignedto'], hyperdb.Multilink): + assignedto_ids = newvalues['assignedto'] + for assignedto_id in assignedto_ids: + new_nosy.add(assignedto_id) + + # see if there's any new messages - if so, possibly add the author and + # recipient to the nosy + if newvalues.has_key('messages'): + if nodeid is None: + ok = ('new', 'yes') + messages = newvalues['messages'] + else: + ok = ('yes',) + # figure which of the messages now on the issue weren't + oldmessages = cl.get(nodeid, 'messages') + messages = [] + for msgid in newvalues['messages']: + if msgid not in oldmessages: + messages.append(msgid) + + # configs for nosy modifications + add_author = getattr(db.config, 'ADD_AUTHOR_TO_NOSY', 'new') + add_recips = getattr(db.config, 'ADD_RECIPIENTS_TO_NOSY', 'new') + + # now for each new message: + msg = db.msg + for msgid in messages: + if add_author in ok: + authid = msg.get(msgid, 'author') + new_nosy.add(authid) + + # add on the recipients of the message + if add_recips in ok: + for recipient in msg.get(msgid, 'recipients'): + new_nosy.add(recipient) + + if current_nosy != new_nosy: + # that's it, save off the new nosy list + newvalues['nosy'] = list(new_nosy) + +def init(db): + db.issue.react('create', nosyreaction) + db.issue.react('set', nosyreaction) + db.issue.audit('create', updatenosy) + db.issue.audit('set', updatenosy) diff --git a/share/roundup/templates/devel/detectors/patches.py b/share/roundup/templates/devel/detectors/patches.py new file mode 100644 index 0000000..d79f367 --- /dev/null +++ b/share/roundup/templates/devel/detectors/patches.py @@ -0,0 +1,45 @@ +# Auditor for patch files +# Patches should be declared as text/plain (also .py files), +# independent of what the browser says, and +# the "patch" keyword should get set automatically. + +import posixpath + +patchtypes = ('.diff', '.patch') +sourcetypes = ('.diff', '.patch', '.py') + +def ispatch(file, types): + return posixpath.splitext(file)[1] in types + +def patches_text_plain(db, cl, nodeid, newvalues): + if ispatch(newvalues['name'], sourcetypes): + newvalues['type'] = 'text/plain' + +def patches_keyword(db, cl, nodeid, newvalues): + # Check whether there are any new files + newfiles = set(newvalues.get('files',())) + if nodeid: + newfiles -= set(db.issue.get(nodeid, 'files')) + # Check whether any of these is a patch + newpatch = False + for fileid in newfiles: + if ispatch(db.file.get(fileid, 'name'), patchtypes): + newpatch = True + break + if newpatch: + # Add the patch keyword if its not already there + patchid = db.keyword.lookup("patch") + oldkeywords = [] + if nodeid: + oldkeywords = db.issue.get(nodeid, 'keywords') + if patchid in oldkeywords: + # This is already marked as a patch + return + if not newvalues.has_key('keywords'): + newvalues['keywords'] = oldkeywords + newvalues['keywords'].append(patchid) + +def init(db): pass +# db.file.audit('create', patches_text_plain) +# db.issue.audit('create', patches_keyword) +# db.issue.audit('set', patches_keyword) diff --git a/share/roundup/templates/devel/detectors/severityauditor.py b/share/roundup/templates/devel/detectors/severityauditor.py new file mode 100644 index 0000000..721c179 --- /dev/null +++ b/share/roundup/templates/devel/detectors/severityauditor.py @@ -0,0 +1,11 @@ + +def init_severity(db, cl, nodeid, newvalues): + """Make sure severity is set on new issues""" + if newvalues.has_key('severity') and newvalues['severity']: + return + + normal = db.severity.lookup('normal') + newvalues['severity'] = normal + +def init(db): pass + #db.issue.audit('create', init_severity) diff --git a/share/roundup/templates/devel/detectors/statusauditor.py b/share/roundup/templates/devel/detectors/statusauditor.py new file mode 100644 index 0000000..e68169f --- /dev/null +++ b/share/roundup/templates/devel/detectors/statusauditor.py @@ -0,0 +1,13 @@ +def preset_new(db, cl, nodeid, newvalues): + """ Make sure the status is set on new issues""" + + if newvalues.has_key('status') and newvalues['status']: + return + + new = db.status.lookup('new') + newvalues['status'] = new + + +def init(db): pass + # fire before changes are made + #db.issue.audit('create', preset_new) diff --git a/share/roundup/templates/devel/detectors/userauditor.py b/share/roundup/templates/devel/detectors/userauditor.py new file mode 100644 index 0000000..b172b56 --- /dev/null +++ b/share/roundup/templates/devel/detectors/userauditor.py @@ -0,0 +1,71 @@ +import re + +# regular expression thanks to: http://www.regular-expressions.info/email.html +# this is the "99.99% solution for syntax only". +email_regexp = (r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*", r"(localhost|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9]))") +email_rfc = re.compile('^' + email_regexp[0] + '@' + email_regexp[1] + '$', re.IGNORECASE) +email_local = re.compile('^' + email_regexp[0] + '$', re.IGNORECASE) + +def valid_address(address): + ''' If we see an @-symbol in the address then check against the full + RFC syntax. Otherwise it is a local-only address so only check + the local part of the RFC syntax. + ''' + if '@' in address: + return email_rfc.match(address) + else: + return email_local.match(address) + +def get_addresses(user): + ''' iterate over all known addresses in a newvalues dict + this takes of the address/alterate_addresses handling + ''' + if user.has_key('address'): + yield user['address'] + if user.get('alternate_addresses', None): + for address in user['alternate_addresses'].split('\n'): + yield address + +def audit_user_fields(db, cl, nodeid, newvalues): + ''' Make sure user properties are valid. + + - email address is syntactically valid + - email address is unique + - roles specified exist + - timezone is valid + ''' + + for address in get_addresses(newvalues): + if not valid_address(address): + print newvalues + raise ValueError, 'Email address syntax is invalid "%s"'%address + + check_main = db.user.stringFind(address=address) + # make sure none of the alts are owned by anyone other than us (x!=nodeid) + check_alts = [x for x in db.user.filter(None, {'alternate_addresses' : address}) if x != nodeid] + if check_main or check_alts: + raise ValueError, 'Email address %s already in use' % address + + for rolename in [r.lower().strip() for r in newvalues.get('roles', '').split(',')]: + if rolename and not db.security.role.has_key(rolename): + raise ValueError, 'Role "%s" does not exist'%rolename + + tz = newvalues.get('timezone', None) + if tz: + # if they set a new timezone validate the timezone by attempting to + # use it before we store it to the db. + import roundup.date + import datetime + try: + TZ = roundup.date.get_timezone(tz) + dt = datetime.datetime.now() + local = TZ.localize(dt).utctimetuple() + except IOError: + raise ValueError, 'Timezone "%s" does not exist' % tz + except ValueError: + raise ValueError, 'Timezone "%s" exceeds valid range [-23...23]' % tz + +def init(db): + # fire before changes are made + db.user.audit('set', audit_user_fields) + db.user.audit('create', audit_user_fields) diff --git a/share/roundup/templates/devel/extensions/local_replace.py b/share/roundup/templates/devel/extensions/local_replace.py new file mode 100644 index 0000000..7523e16 --- /dev/null +++ b/share/roundup/templates/devel/extensions/local_replace.py @@ -0,0 +1,35 @@ +import re + +substitutions = [ (re.compile('debian:\#(?P\d+)'), + 'debian#\g' ), + (re.compile('\#(?P\s*)(?P\d+)'), + "#\g\g" ), + (re.compile('(?P\s+)revision(?P\s*)(?P\d+)'), + "\grevision\g\g"), + (re.compile('(?P\s+)rev(?P\s*)(?P\d+)'), + "\grev\g\g"), + (re.compile('(?P\s+)(?Pr|r\s+)(?P\d+)'), + "\g\g\g"), + ] + +def local_replace(message): + + for cre, replacement in substitutions: + message = cre.sub(replacement, message) + + return message + + + +def init(instance): + instance.registerUtil('localReplace', local_replace) + + +if "__main__" == __name__: + print " debian:#222", local_replace(" debian:#222") + print " revision 222", local_replace(" revision 222") + print " wordthatendswithr 222", local_replace(" wordthatendswithr 222") + print " r222", local_replace(" r222") + print " r 222", local_replace(" r 222") + print " #555", local_replace(" #555") + diff --git a/share/roundup/templates/devel/extensions/spambayes.py b/share/roundup/templates/devel/extensions/spambayes.py new file mode 100644 index 0000000..b6ab303 --- /dev/null +++ b/share/roundup/templates/devel/extensions/spambayes.py @@ -0,0 +1,84 @@ +import re, math +from roundup.cgi.actions import Action +from roundup.cgi.exceptions import * + +import xmlrpclib, socket + +REVPAT = re.compile(r'(r[0-9]+\b|rev(ision)? [0-9]+\b)') + +def extract_classinfo(db, classname, nodeid): + node = db.getnode(classname, nodeid) + + authorage = node['creation'].timestamp() - \ + db.getnode('user', node.get('author', node.get('creator')))['creation'].timestamp() + + authorid = node.get('author', node.get('creator')) + + content = db.getclass(classname).get(nodeid, 'content') + + tokens = ["klass:%s" % classname, + "author:%s" % authorid, + "authorage:%d" % int(math.log(authorage)), + "hasrev:%s" % (REVPAT.search(content) is not None)] + + return (content, tokens) + +def train_spambayes(db, content, tokens, is_spam): + spambayes_uri = db.config.detectors['SPAMBAYES_URI'] + + server = xmlrpclib.ServerProxy(spambayes_uri, verbose=False) + try: + server.train({'content':content}, tokens, {}, is_spam) + return (True, None) + except (socket.error, xmlrpclib.Error), e: + return (False, str(e)) + + +class SpambayesClassify(Action): + permissionType = 'SB: May Classify' + + def handle(self): + (content, tokens) = extract_classinfo(self.db, + self.classname, self.nodeid) + + if self.form.has_key("trainspam"): + is_spam = True + elif self.form.has_key("trainham"): + is_spam = False + + (status, errmsg) = train_spambayes(self.db, content, tokens, + is_spam) + + node = self.db.getnode(self.classname, self.nodeid) + props = {} + + if status: + if node.get('spambayes_misclassified', False): + props['spambayes_misclassified'] = True + + props['spambayes_score'] = 1.0 + + s = " SPAM" + if not is_spam: + props['spambayes_score'] = 0.0 + s = " HAM" + self.client.ok_message.append(self._('Message classified as') + s) + else: + self.client.error_message.append(self._('Unable to classify message, got error:') + errmsg) + + klass = self.db.getclass(self.classname) + klass.set(self.nodeid, **props) + self.db.commit() + +def sb_is_spam(obj): + cutoff_score = float(obj._db.config.detectors['SPAMBAYES_SPAM_CUTOFF']) + try: + score = obj['spambayes_score'] + except KeyError: + return False + return score >= cutoff_score + +def init(instance): + instance.registerAction("spambayes_classify", SpambayesClassify) + instance.registerUtil('sb_is_spam', sb_is_spam) + diff --git a/share/roundup/templates/devel/extensions/timestamp.py b/share/roundup/templates/devel/extensions/timestamp.py new file mode 100644 index 0000000..270e480 --- /dev/null +++ b/share/roundup/templates/devel/extensions/timestamp.py @@ -0,0 +1,28 @@ +import time, struct, base64 +from roundup.cgi.actions import RegisterAction +from roundup.cgi.exceptions import * + +def timestamp(): + return base64.encodestring(struct.pack("i", time.time())).strip() + +def unpack_timestamp(s): + return struct.unpack("i",base64.decodestring(s))[0] + +class Timestamped: + def check(self): + try: + created = unpack_timestamp(self.form['opaque'].value) + except KeyError: + raise FormError, "somebody tampered with the form" + if time.time() - created < 4: + raise FormError, "responding to the form too quickly" + return True + +class TimestampedRegister(Timestamped, RegisterAction): + def permission(self): + self.check() + RegisterAction.permission(self) + +def init(instance): + instance.registerUtil('timestamp', timestamp) + instance.registerAction('register', TimestampedRegister) diff --git a/share/roundup/templates/devel/extensions/timezone.py b/share/roundup/templates/devel/extensions/timezone.py new file mode 100644 index 0000000..2a994c8 --- /dev/null +++ b/share/roundup/templates/devel/extensions/timezone.py @@ -0,0 +1,37 @@ +# Utility for replacing the simple input field for the timezone with +# a select-field that lists the available values. + +import cgi + +try: + import pytz +except ImportError: + pytz = None + + +def tzfield(prop, name, default): + if pytz: + value = prop.plain() + if '' == value: + value = default + else: + try: + value = "Etc/GMT%+d" % int(value) + except ValueError: + pass + + l = ['') + return '\n'.join(l) + + else: + return prop.field() + +def init(instance): + instance.registerUtil('tzfield', tzfield) diff --git a/share/roundup/templates/devel/html/_generic.404.html b/share/roundup/templates/devel/html/_generic.404.html new file mode 100644 index 0000000..71c9e0e --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.404.html @@ -0,0 +1,9 @@ + + +Item Not Found + + + +There is no with id + + diff --git a/share/roundup/templates/devel/html/_generic.calendar.html b/share/roundup/templates/devel/html/_generic.calendar.html new file mode 100644 index 0000000..0f9cf40 --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.calendar.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/share/roundup/templates/devel/html/_generic.collision.html b/share/roundup/templates/devel/html/_generic.collision.html new file mode 100644 index 0000000..a701c58 --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.collision.html @@ -0,0 +1,16 @@ + +<span tal:replace="python:context._classname.capitalize()" + i18n:name="class" /> Edit Collision - <span i18n:name="tracker" + tal:replace="config/TRACKER_NAME" /> + Edit Collision + + + + diff --git a/share/roundup/templates/devel/html/_generic.help-empty.html b/share/roundup/templates/devel/html/_generic.help-empty.html new file mode 100644 index 0000000..65469fc --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.help-empty.html @@ -0,0 +1,8 @@ + + + Empty page (no search performed yet) + + +

Please specify your search parameters!

+ + diff --git a/share/roundup/templates/devel/html/_generic.help-list.html b/share/roundup/templates/devel/html/_generic.help-list.html new file mode 100644 index 0000000..4c71338 --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.help-list.html @@ -0,0 +1,55 @@ + +

You are not + allowed to view this page.

+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
 x
+ + + +
+
+
+
+ +
+     
+
diff --git a/share/roundup/templates/devel/html/_generic.help-search.html b/share/roundup/templates/devel/html/_generic.help-search.html
new file mode 100644
index 0000000..01d657e
--- /dev/null
+++ b/share/roundup/templates/devel/html/_generic.help-search.html
@@ -0,0 +1,13 @@
+
+  
+    Frame for search input fields
+  
+  
+    

Generic template + help-search + or version for class + user + is not yet implemented

+ + + diff --git a/share/roundup/templates/devel/html/_generic.help-submit.html b/share/roundup/templates/devel/html/_generic.help-submit.html new file mode 100644 index 0000000..b13aa4a --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.help-submit.html @@ -0,0 +1,73 @@ + + + + + + Generic submit page for framed helper windows + + + + + +
+ 
+
+ +
+ + + + +
+ + + diff --git a/share/roundup/templates/devel/html/_generic.help.html b/share/roundup/templates/devel/html/_generic.help.html new file mode 100644 index 0000000..cfa941c --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.help.html @@ -0,0 +1,140 @@ + + + +
+ +
+ + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
 x
+ + + +
 x
+ +
+ +
+ + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+
+
+
+
diff --git a/share/roundup/templates/devel/html/_generic.index.html b/share/roundup/templates/devel/html/_generic.index.html new file mode 100644 index 0000000..53b0a36 --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.index.html @@ -0,0 +1,68 @@ + +<span tal:replace="python:context._classname.capitalize()" + i18n:name="class" /> editing - <span i18n:name="tracker" + tal:replace="config/TRACKER_NAME" /> + editing + + + +You are not allowed to view this page. + +Please login with your username and password. + + + +

+ You may edit the contents of the + + class using this form. Commas, newlines and double quotes (") must be + handled delicately. You may include commas and newlines by enclosing the + values in double-quotes ("). Double quotes themselves must be quoted by + doubling (""). +

+ +

+ Multilink properties have their multiple values colon (":") separated + (... ,"one:two:three", ...) +

+ +

+ Remove entries by deleting their line. Add new entries by appending + them to the table - put an X in the id column. +

+
+
+ +
+ + +
+
+ + + + + + + + + + +
 
 
+ + + +
diff --git a/share/roundup/templates/devel/html/_generic.item.html b/share/roundup/templates/devel/html/_generic.item.html new file mode 100644 index 0000000..077225f --- /dev/null +++ b/share/roundup/templates/devel/html/_generic.item.html @@ -0,0 +1,53 @@ + +<span tal:replace="python:context._classname.capitalize()" + i18n:name="class" /> editing - <span i18n:name="tracker" + tal:replace="config/TRACKER_NAME" /> + editing + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ +
+ + + + + + + + + + + + + + + +
  + submit button will go here +
+ +
+ + + +
+ + + +
diff --git a/share/roundup/templates/devel/html/bug.index.html b/share/roundup/templates/devel/html/bug.index.html new file mode 100644 index 0000000..377dc42 --- /dev/null +++ b/share/roundup/templates/devel/html/bug.index.html @@ -0,0 +1,179 @@ + + + <span tal:omit-tag="true" i18n:translate="" >List of bugs</span> + <span tal:condition="request/dispname" + tal:replace="python:' - %s '%request.dispname" + /> - <span tal:replace="config/TRACKER_NAME" /> + + + List of bugs + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SeverityIDCreationActivityActorTitleComponentsVersionsStatusResolutionCreatorAssigned ToKeywordsDepends OnType
+ + + +
      + title +          
+ + + + + + +
+
+ +
+ + + + + + + + + + + + +
+ Group on: + + + Descending: +
+ + +
+
+ +
+
+
diff --git a/share/roundup/templates/devel/html/bug.item.html b/share/roundup/templates/devel/html/bug.item.html new file mode 100644 index 0000000..6fac895 --- /dev/null +++ b/share/roundup/templates/devel/html/bug.item.html @@ -0,0 +1,324 @@ + + +<tal:block condition="context/id" i18n:translate="" + >Bug <span tal:replace="context/id" i18n:name="id" + />: <span tal:replace="context/title" i18n:name="title" + /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:block> +<tal:block condition="not:context/id" i18n:translate="" + >New Bug report - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:block> + + + New Bug + New Bug Editing + Bug + Bug Editing + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ +
+ +
classification + + + + + + + + + + + + + + + + + + + + +
Title:title + + +
+ + Type: + type + + Severity: + severity
+ + Components: + components + + Versions: + versions
+
+ +
process + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Status: + status + + Resolution: + resolution
+ Dependencies: + + + + +
View: +
+
+ Superseder: + + + + + +
View: + +
+
Assigned To:assignedto menuNosy List: + + + +
+ + Priority: + priorityKeywords:keywords
Comment: + +
File: + + +
File Description:
+
+ + + + + +
+   + + + + submit button + Make a copy +
+
+ +

+ Created on + by , + last changed + by . +

+ + + + + + + + + + + + + + + + +
Files
File nameUploadedDescriptionEditRemove
+ dld link + + creator's name, + creation date + + edit + +
+ + + +
+
+ + + + + + + + + + + + + + +
Messages
msgAuthor: ()Date: +
+ + + +
+
+
content
+
+ + + +
+ + + +
diff --git a/share/roundup/templates/devel/html/bug.search.html b/share/roundup/templates/devel/html/bug.search.html new file mode 100644 index 0000000..4bfd665 --- /dev/null +++ b/share/roundup/templates/devel/html/bug.search.html @@ -0,0 +1,311 @@ + +Bug searching - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +Bug searching + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Filter onDisplaySort onGroup on
All text*:   
Title: 
ID: 
Creation Date:
Creator: + +
Activity: 
Last actor: + +  
Nosy list member: 
Type: + +
Components: + +
Versions: + +
Depends on: +
Assigned to: + + + + +
Keyword: + +
Priority: + +
Status: + + + + +
Resolution: +
No Sort or group:  
Pagesize:
Start With:
Sort Descending: +
Group Descending: +
Query name**: + + +
+   + +
  + + *: The "all text" field will look in message bodies and bug titles +
+ + **: If you supply a name, the query will be saved off and available as a + link in the sidebar + +
+ +
+ + +
diff --git a/share/roundup/templates/devel/html/favicon.ico b/share/roundup/templates/devel/html/favicon.ico new file mode 100644 index 0000000..c9efc58 Binary files /dev/null and b/share/roundup/templates/devel/html/favicon.ico differ diff --git a/share/roundup/templates/devel/html/file.index.html b/share/roundup/templates/devel/html/file.index.html new file mode 100644 index 0000000..7072172 --- /dev/null +++ b/share/roundup/templates/devel/html/file.index.html @@ -0,0 +1,32 @@ + +List of files - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" /> +List of files + + + + + + + + + + + + + + + + + + + +
DownloadDescriptoinContent TypeUploaded ByDate
+ dld link + descriptioncontent typecreator's namecreation date
+ + + +
diff --git a/share/roundup/templates/devel/html/file.item.html b/share/roundup/templates/devel/html/file.item.html new file mode 100644 index 0000000..d6f4f7f --- /dev/null +++ b/share/roundup/templates/devel/html/file.item.html @@ -0,0 +1,78 @@ + +File display - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +File display + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ + + + + + + + + + + + + + + + + + +
Name
Description
Content Type + Please note that + for security reasons, it's not permitted to set content type to text/html.
+   + + + + submit button here
+
+ + + +download + + + + + + + + + + + diff --git a/share/roundup/templates/devel/html/help.css b/share/roundup/templates/devel/html/help.css new file mode 100644 index 0000000..31f4ab1 --- /dev/null +++ b/share/roundup/templates/devel/html/help.css @@ -0,0 +1,29 @@ +@import url(style.css); + +body { padding: 0;} +body > .header, +body > .footer +{ + margin: 0; +} +body .content { width: auto;} +.header h1 { font-size: 110%; } + +table.classhelp th +{ + border-top: 1px solid #afafaf; + border-bottom: 1px solid #afafaf; + empty-cells: show; + text-align: left; + vertical-align: middle; + white-space: nowrap; +} + +table.classhelp td +{ + border-bottom: 1px solid #efefef; + empty-cells: show; + text-align: left; + vertical-align: middle; + white-space: nowrap; +} diff --git a/share/roundup/templates/devel/html/help.html b/share/roundup/templates/devel/html/help.html new file mode 100644 index 0000000..ba7f461 --- /dev/null +++ b/share/roundup/templates/devel/html/help.html @@ -0,0 +1,69 @@ + + + + + + + <tal:x i18n:name="what" tal:content="what" i18n:translate="" /> + help - <span i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> + + + + + + + +
+

Roundup Issue Tracker: Help - item

+
+
+ Page content goes here +
+ + + +
+ + + + + x + + + + + + + + + + +
diff --git a/share/roundup/templates/devel/html/help_controls.js b/share/roundup/templates/devel/html/help_controls.js new file mode 100644 index 0000000..775b578 --- /dev/null +++ b/share/roundup/templates/devel/html/help_controls.js @@ -0,0 +1,324 @@ +// initial values for either Nosy, Superseder, Topic and Waiting On, +// depending on which has called +original_field = form[field].value; + +// Some browsers (ok, IE) don't define the "undefined" variable. +undefined = document.geez_IE_is_really_friggin_annoying; + +function trim(value) { + var temp = value; + var obj = /^(\s*)([\W\w]*)(\b\s*$)/; + if (obj.test(temp)) { temp = temp.replace(obj, '$2'); } + var obj = / /g; + while (temp.match(obj)) { temp = temp.replace(obj, " "); } + return temp; +} + +function determineList() { + // generate a comma-separated list of the checked items + var list = new String(''); + + // either a checkbox object or an array of checkboxes + var check = document.frm_help.check; + + if ((check.length == undefined) && (check.checked != undefined)) { + // only one checkbox on page + if (check.checked) { + list = check.value; + } + } else { + // array of checkboxes + for (box=0; box < check.length; box++) { + if (check[box].checked) { + if (list.length == 0) { + separator = ''; + } + else { + separator = ','; + } + // we used to use an Array and push / join, but IE5.0 sux + list = list + separator + check[box].value; + } + } + } + return list; +} + +/** + * update the field in the opening window; + * the text_field variable must be set in the calling page + */ +function updateOpener() { + // write back to opener window + if (document.frm_help.check==undefined) { return; } + form[field].value = text_field.value; +} + +function updateList() { + // write back to opener window + if (document.frm_help.check==undefined) { return; } + form[field].value = determineList(); +} + +function updatePreview() { + // update the preview box + if (document.frm_help.check==undefined) { return; } + writePreview(determineList()); +} + +function clearList() { + // uncheck all checkboxes + if (document.frm_help.check==undefined) { return; } + for (box=0; box < document.frm_help.check.length; box++) { + document.frm_help.check[box].checked = false; + } +} + +function reviseList_framed(form, textfield) { + // update the checkboxes based on the preview field + // alert('reviseList_framed') + // alert(form) + if (form.check==undefined) + return; + // alert(textfield) + var to_check; + var list = textfield.value.split(","); + if (form.check.length==undefined) { + check = form.check; + to_check = false; + for (val in list) { + if (check.value==trim(list[val])) { + to_check = true; + break; + } + } + check.checked = to_check; + } else { + for (box=0; box < form.check.length; box++) { + check = form.check[box]; + to_check = false; + for (val in list) { + if (check.value==trim(list[val])) { + to_check = true; + break; + } + } + check.checked = to_check; + } + } +} + +function reviseList(vals) { + // update the checkboxes based on the preview field + if (document.frm_help.check==undefined) { return; } + var to_check; + var list = vals.split(","); + if (document.frm_help.check.length==undefined) { + check = document.frm_help.check; + to_check = false; + for (val in list) { + if (check.value==trim(list[val])) { + to_check = true; + break; + } + } + check.checked = to_check; + } else { + for (box=0; box < document.frm_help.check.length; box++) { + check = document.frm_help.check[box]; + to_check = false; + for (val in list) { + if (check.value==trim(list[val])) { + to_check = true; + break; + } + } + check.checked = to_check; + } + } +} + +function resetList() { + // reset preview and check boxes to initial values + if (document.frm_help.check==undefined) { return; } + writePreview(original_field); + reviseList(original_field); +} + +function writePreview(val) { + // writes a value to the text_preview + document.frm_help.text_preview.value = val; +} + +function focusField(name) { + for(i=0; i < document.forms.length; ++i) { + var obj = document.forms[i].elements[name]; + if (obj && obj.focus) {obj.focus();} + } +} + +function selectField(name) { + for(i=0; i < document.forms.length; ++i) { + var obj = document.forms[i].elements[name]; + if (obj && obj.focus){obj.focus();} + if (obj && obj.select){obj.select();} + } +} + +function checkRequiredFields(fields) +{ + var bonk=''; + var res=''; + var argv = checkRequiredFields.arguments; + var argc = argv.length; + var input = ''; + var val=''; + + for (var i=0; i < argc; i++) { + fi = argv[i]; + input = document.getElementById(fi); + if (input) { + val = input.value + if (val == '' || val == '-1' || val == -1) { + if (res == '') { + res = fi; + bonk = input; + } else { + res += ', '+fi; + } + } + } else { + alert('Field with id='+fi+' not found!') + } + } + if (res == '') { + return submit_once(); + } else { + alert('Missing value here ('+res+')!'); + if (window.event && window.event.returnvalue) { + event.returnValue = 0; // work-around for IE + } + bonk.focus(); + return false; + } +} + +/** + * seeks the given value (2nd argument) + * in the value of the given input element (1st argument), + * which is considered a list of values, separated by commas + */ +function has_value(input, val) +{ + var actval = input.value + var arr = feld.value.split(','); + var max = arr.length; + for (i=0;i remove_val() + * + * This will work nicely even for batched lists + */ +function append_val(name, val) +{ + var feld = document.itemSynopsis[name]; + var actval = feld.value; + if (actval == '') { + feld.value = val + } else { + var arr = feld.value.split(','); + var max = arr.length; + for (i=0;i append_val() + */ +function remove_val(name, val) +{ + var feld = document.itemSynopsis[name]; + var actval = feld.value; + var changed=false; + if (actval == '') { + return + } else { + var arr = feld.value.split(','); + var max = arr.length; + var neu = '' + for (i=0;i +List of classes - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +List of classes + + + + + + + + + + + + + +
+ classname +
nametype
+ + +
diff --git a/share/roundup/templates/devel/html/home.html b/share/roundup/templates/devel/html/home.html new file mode 100644 index 0000000..09aadd9 --- /dev/null +++ b/share/roundup/templates/devel/html/home.html @@ -0,0 +1,10 @@ + + diff --git a/share/roundup/templates/devel/html/jquery.js b/share/roundup/templates/devel/html/jquery.js new file mode 100644 index 0000000..9263574 --- /dev/null +++ b/share/roundup/templates/devel/html/jquery.js @@ -0,0 +1,4376 @@ +/*! + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){ + +var + // Will speed up references to window, and allows munging its name. + window = this, + // Will speed up references to undefined, and allows munging its name. + undefined, + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + // Map over the $ in case of overwrite + _$ = window.$, + + jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + this.context = selector; + return this; + } + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem && elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + var ret = jQuery( elem || [] ); + ret.context = document; + ret.selector = selector; + return ret; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document ).ready( selector ); + + // Make sure that old selector state is passed along + if ( selector.selector && selector.context ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return this.setArray(jQuery.isArray( selector ) ? + selector : + jQuery.makeArray(selector)); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.3.2", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num === undefined ? + + // Return a 'clean' array + Array.prototype.slice.call( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) + ret.selector = this.selector + (this.selector ? " " : "") + selector; + else if ( name ) + ret.selector = this.selector + "." + name + "(" + selector + ")"; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( typeof name === "string" ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text !== "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).clone(); + + if ( this[0].parentNode ) + wrap.insertBefore( this[0] ); + + wrap.map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }).append(this); + } + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: [].push, + sort: [].sort, + splice: [].splice, + + find: function( selector ) { + if ( this.length === 1 ) { + var ret = this.pushStack( [], "find", selector ); + ret.length = 0; + jQuery.find( selector, this[0], ret ); + return ret; + } else { + return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + })), "find", selector ); + } + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var html = this.outerHTML; + if ( !html ) { + var div = this.ownerDocument.createElement("div"); + div.appendChild( this.cloneNode(true) ); + html = div.innerHTML; + } + + return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; + } else + return this.cloneNode(true); + }); + + // Copy the events from the original to the clone + if ( events === true ) { + var orig = this.find("*").andSelf(), i = 0; + + ret.find("*").andSelf().each(function(){ + if ( this.nodeName !== orig[i].nodeName ) + return; + + var events = jQuery.data( orig[i], "events" ); + + for ( var type in events ) { + for ( var handler in events[ type ] ) { + jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); + } + } + + i++; + }); + } + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ + return elem.nodeType === 1; + }) ), "filter", selector ); + }, + + closest: function( selector ) { + var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, + closer = 0; + + return this.map(function(){ + var cur = this; + while ( cur && cur.ownerDocument ) { + if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { + jQuery.data(cur, "closest", closer); + return cur; + } + cur = cur.parentNode; + closer++; + } + }); + }, + + not: function( selector ) { + if ( typeof selector === "string" ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector === "string" ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return !!selector && this.is( "." + selector ); + }, + + val: function( value ) { + if ( value === undefined ) { + var elem = this[0]; + + if ( elem ) { + if( jQuery.nodeName( elem, 'option' ) ) + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if ( typeof value === "number" ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value === undefined ? + (this[0] ? + this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, +i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ), + "slice", Array.prototype.slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + domManip: function( args, table, callback ) { + if ( this[0] ) { + var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), + scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), + first = fragment.firstChild; + + if ( first ) + for ( var i = 0, l = this.length; i < l; i++ ) + callback.call( root(this[i], first), this.length > 1 || i > 0 ? + fragment.cloneNode(true) : fragment ); + + if ( scripts ) + jQuery.each( scripts, evalScript ); + } + + return this; + + function root( elem, cur ) { + return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; + } + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy === "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +// exclude the following css properties to add px +var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}, + toString = Object.prototype.toString; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return toString.call(obj) === "[object Function]"; + }, + + isArray: function( obj ) { + return toString.call(obj) === "[object Array]"; + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && /\S/.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.support.scriptEval ) + script.appendChild( document.createTextNode( data ) ); + else + script.text = data; + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length === undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length === undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames !== undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force, extra ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + + if ( extra === "border" ) + return; + + jQuery.each( which, function() { + if ( !extra ) + val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + if ( extra === "margin" ) + val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; + else + val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + } + + if ( elem.offsetWidth !== 0 ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, Math.round(val)); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // We need to handle opacity special in IE + if ( name == "opacity" && !jQuery.support.opacity ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle ) + ret = computedStyle.getPropertyValue( name ); + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context, fragment ) { + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { + var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); + if ( match ) + return [ context.createElement( match[1] ) ]; + } + + var ret = [], scripts = [], div = context.createElement("div"); + + jQuery.each(elems, function(i, elem){ + if ( typeof elem === "number" ) + elem += ''; + + if ( !elem ) + return; + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + + + New Keyword + New Keyword Editing + Keyword + Keyword Editing + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
Keyword:title
Description:description
+   + + + + +
+
+
+ + diff --git a/share/roundup/templates/devel/html/milestone.index.html b/share/roundup/templates/devel/html/milestone.index.html new file mode 100644 index 0000000..0e45817 --- /dev/null +++ b/share/roundup/templates/devel/html/milestone.index.html @@ -0,0 +1,144 @@ + + + <span tal:omit-tag="true" i18n:translate="" >List of milestones</span> + <span tal:condition="request/dispname" + tal:replace="python:' - %s '%request.dispname" + /> - <span tal:replace="config/TRACKER_NAME" /> + + + List of milestones + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDTitleStatus
+ + + +
  + title +  
+ + + + + + +
+
+ +
+ + + + + + + + + + + + +
+ Group on: + + + Descending: +
+ + +
+
+ +
+
+
diff --git a/share/roundup/templates/devel/html/milestone.item.html b/share/roundup/templates/devel/html/milestone.item.html new file mode 100644 index 0000000..7501928 --- /dev/null +++ b/share/roundup/templates/devel/html/milestone.item.html @@ -0,0 +1,179 @@ + + + + +

Milestone Editing

+
+ + + + + + + +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Title + title + + title +
last changed +
+ status + + status + + status +
Bugs + + + + + + + + +
Add:
+   + + + remove +
+
+ +
View: + +   + +
+
Add Comment: + +
File
+ CC List: + + +
+ + + + +

+ submit button will go here +

+ + + + + + + + + + + + + + +
Messages
authordate
content
+ + + + + + + + + +
Files
File nameUploaded
+ dld link + + creator's name, + creation date +
+ + + + + + + + +
+ +You are not allowed to view this page. + + +
+
+ diff --git a/share/roundup/templates/devel/html/msg.index.html b/share/roundup/templates/devel/html/msg.index.html new file mode 100644 index 0000000..1ad4b33 --- /dev/null +++ b/share/roundup/templates/devel/html/msg.index.html @@ -0,0 +1,26 @@ + +List of messages - <span tal:replace="config/TRACKER_NAME" + i18n:name="tracker"/> +Message listing + + + + + + + + + + + + + + + + +
Messages
authordate
content
+ + +
diff --git a/share/roundup/templates/devel/html/msg.item.html b/share/roundup/templates/devel/html/msg.item.html new file mode 100644 index 0000000..ebe16eb --- /dev/null +++ b/share/roundup/templates/devel/html/msg.item.html @@ -0,0 +1,123 @@ + + +<tal:block condition="context/id" i18n:translate="" + >Message <span tal:replace="context/id" i18n:name="id" + /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:block> +<tal:block condition="not:context/id" i18n:translate="" + >New Message - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:block> + + + New Message + New Message Editing + Message + Message Editing + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Author
Recipients
Date
Message-id
In-reply-to
+ + + + + + + + + + + +
+
+ You are not authorized to see this message. + +
+ + + + + + + + + + + +
Files
File nameUploaded
+ dld link + + creator's name, + creation date +
+ + + +
+ + +
diff --git a/share/roundup/templates/devel/html/page.html b/share/roundup/templates/devel/html/page.html new file mode 100644 index 0000000..339c0bd --- /dev/null +++ b/share/roundup/templates/devel/html/page.html @@ -0,0 +1,407 @@ + + + + + title goes here + + + + + + +
+

Roundup Demo Tracker

+ +
+ +
+

body title

+

+

+ + clear this message +

+ Page content goes here +
+ +

+ 
+
+
+ + + + + + + + + + + + + + + + (cal) + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
  • + +
  • +
  • + + +
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/share/roundup/templates/devel/html/patch.index.html b/share/roundup/templates/devel/html/patch.index.html new file mode 100644 index 0000000..a9186c4 --- /dev/null +++ b/share/roundup/templates/devel/html/patch.index.html @@ -0,0 +1,66 @@ + + + List of patches - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" /> + +List of patches + + + + + + + + + + + + + + + + + + + +
DownloadDescriptionContent TypeUploaded ByDate
+ dld link + descriptioncontent typecreator's namecreation date
+ + + +
+ + + List of patches - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" /> + +List of patches + + + + + + + + + + + + + + + + + + + +
DownloadDescriptionContent TypeUploaded ByDate
+ dld link + descriptioncontent typecreator's namecreation date
+ + + +
diff --git a/share/roundup/templates/devel/html/patch.item.html b/share/roundup/templates/devel/html/patch.item.html new file mode 100644 index 0000000..b616ac1 --- /dev/null +++ b/share/roundup/templates/devel/html/patch.item.html @@ -0,0 +1,172 @@ + +Patch display - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +Modify patch + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Name
Description
Content Type + Please note that + for security reasons, it's not permitted to set content type to text/html.
Repository
Repository revision
+   + + + + submit button here
+
+ + + +download + + + + + + + + + + + + +Patch display - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +Patch display + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Name
Description
Content Type + Please note that + for security reasons, it's not permitted to set content type to text/html.
Repository
Repository revision
+   + + + + submit button here
+
+ + + +download + + + + + + + + + + + diff --git a/share/roundup/templates/devel/html/query.edit.html b/share/roundup/templates/devel/html/query.edit.html new file mode 100644 index 0000000..3afc099 --- /dev/null +++ b/share/roundup/templates/devel/html/query.edit.html @@ -0,0 +1,108 @@ + +"Your Queries" Editing - <span tal:replace="config/TRACKER_NAME" + i18n:name="tracker" /> +"Your Queries" Editing + + + +You are not allowed to edit queries. + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QueryInclude in "Your Queries"EditPrivate to you? 
query + + + [query is retired]
query + + edit + + + +
query + + + edit + [not yours to edit]
+ + + +
+ +
+ +
diff --git a/share/roundup/templates/devel/html/query.item.html b/share/roundup/templates/devel/html/query.item.html new file mode 100644 index 0000000..453e13d --- /dev/null +++ b/share/roundup/templates/devel/html/query.item.html @@ -0,0 +1,3 @@ + + + diff --git a/share/roundup/templates/devel/html/search.js b/share/roundup/templates/devel/html/search.js new file mode 100644 index 0000000..86d4eef --- /dev/null +++ b/share/roundup/templates/devel/html/search.js @@ -0,0 +1,31 @@ +// Inspect a form element to construct a 'get' request, +// register it to the 'submit' event, and deactivate the +// form's submit action. +function bind_search() +{ + var form = $("form"); + var action = form.attr("action"); + form.attr("action",""); // reset + + function display(data) + { + var list = $("div.list"); + list.empty(); + list.append(data); + } + + function query() + { + var inputs = $(":input"); + var data = {} + for (var i = 0; i < inputs.length; i++) + data[inputs[i].name] = inputs[i].value; + jQuery.get(action, data, display); + return false; + } + + form.submit(query); +} + + +$(document).ready(bind_search); diff --git a/share/roundup/templates/devel/html/style.css b/share/roundup/templates/devel/html/style.css new file mode 100644 index 0000000..0372f36 --- /dev/null +++ b/share/roundup/templates/devel/html/style.css @@ -0,0 +1,138 @@ +/* layout*/ +body +{ + font-family: sans-serif, Arial, Helvetica; + background-color: white; + color: #333; + margin:0; + padding: 0 3em 0 13em; +} +body > .header { margin: 0 0 0 -13em;} +body > .footer { margin: 0 0 0 -13em; clear:both;} +body > .navigation +{ + margin-left: -13em; + width: 13em; + float: left; +} +body > .content +{ + width: 100%; + margin: 0; +} +body > .header > #searchbox { position: absolute; right: 1em; top: 1em;} + +/* style */ + +.footer { padding: 1em;} + +:link { color: #bb0000; text-decoration: none;} +:visited { color: #770000; text-decoration: none;} + +.header h1 { margin-left: 1em; } + +body +{ + font-family: sans-serif, Arial, Helvetica; + background-color: #f5f5f5; + color: #333; +} + +.menu { padding: 0; margin-right: 1em;} +.menu ul +{ + padding: 0; + margin: 0; +} +.menu li +{ + margin: 5pt 0; +} +.menu > ul > li > * +{ + display: block; + padding: 2pt 2pt 2pt 10pt; + border: solid thin #dadada; + background-color:#ffffff; +} +.menu > ul > li.current > * +{ + background-color:#dddddd; +} + +.menu ul li:first-child { margin-top:0;} +.menu ul { list-style-type:none;} + +/* sub-menus are indented */ +.menu > ul > li > ul, +.menu > ul > li.current > ul +{ + border: none; + background-color: inherit; + margin: 0; +} +.menu ul ul +{ + margin-left: 2em; + font-size: smaller; + margin: 0 0 0 5pt; +} + +/* sub-menu items draw a separator */ +.menu ul ul > li +{ + margin: 0; + padding: 0; + border: none; + border-top: solid thin #dadada; + background-color: inherit; +} +.menu ul ul > li:first-child +{ + border-top: none; +} + +.footer +{ + font-size: small; + text-align: center; + color: lightgrey; +} + +.content +{ + padding: 1em; + border: solid thin #dadada; + background-color: #ffffff; +} + +#search { text-align: right; } + +p.ok-message +{ + background-color: #eeffee; + border: 1px solid #009900; + padding: 5px; + color: #009900; + font-weight: bold; +} +p.error-message +{ + background-color: #ffeeee; + border: 1px solid #990000; + padding: 5px; + color: #990000; + font-weight: bold; +} + +table +{ + border-collapse: collapse; + border-spacing: 1px; + width: 100%; + background-color: #fafafa; +} + +tr.odd { background-color:#f5f5f5; } + +input, textarea { border-width: 1px; } diff --git a/share/roundup/templates/devel/html/task.index.html b/share/roundup/templates/devel/html/task.index.html new file mode 100644 index 0000000..1080dfe --- /dev/null +++ b/share/roundup/templates/devel/html/task.index.html @@ -0,0 +1,179 @@ + + + <span tal:omit-tag="true" i18n:translate="" >List of tasks</span> + <span tal:condition="request/dispname" + tal:replace="python:' - %s '%request.dispname" + /> - <span tal:replace="config/TRACKER_NAME" /> + + + List of tasks + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SeverityIDCreationActivityActorTitleComponentsVersionsStatusResolutionCreatorAssigned ToKeywordsDepends OnType
+ + + +
      + title +          
+ + + + + + +
+
+ +
+ + + + + + + + + + + + +
+ Group on: + + + Descending: +
+ + +
+
+ +
+
+
diff --git a/share/roundup/templates/devel/html/task.item.html b/share/roundup/templates/devel/html/task.item.html new file mode 100644 index 0000000..7c51b2d --- /dev/null +++ b/share/roundup/templates/devel/html/task.item.html @@ -0,0 +1,235 @@ + + +<tal:block condition="context/id" i18n:translate="" + >Task <span tal:replace="context/id" i18n:name="id" + />: <span tal:replace="context/title" i18n:name="title" + /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:block> +<tal:block condition="not:context/id" i18n:translate="" + >New Task - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:block> + + + New Task + New Task Editing + Task + Task Editing + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ +
+ +
classification + + + + + + + + + + + + + +
Title:title + + +
+ : + type + : + components
+
+ +
process + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ : + statusResolution:resolution
+ Dependencies: + + + + +
View: +
+
+ Superseder: + + + + + +
View: + +
+
Assigned To:assignedto menuNosy List: + + + +
+ : + priority
Comment: + +
File: + + +
File Description:
+
+ + + + + +
+   + + + + submit button + Make a copy +
+
+ +

+ Created on + by , + last changed + by . +

+ + + + + + + + + + + + + + + + +
Files
File nameUploadedDescriptionEditRemove
+ dld link + + creator's name, + creation date + + edit + +
+ + + +
+
+ + + + + + + + + + + + + + +
Messages
msgAuthor: ()Date: +
+ + + +
+
+
content
+
+ + + +
+ + + +
diff --git a/share/roundup/templates/devel/html/task.search.html b/share/roundup/templates/devel/html/task.search.html new file mode 100644 index 0000000..9679cfb --- /dev/null +++ b/share/roundup/templates/devel/html/task.search.html @@ -0,0 +1,311 @@ + +Task searching - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +Task searching + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Filter onDisplaySort onGroup on
All text*:   
Title: 
ID: 
Creation Date:
Creator: + +
Activity: 
Last actor: + +  
Nosy list member: 
Type: + +
Components: + +
Versions: + +
Depends on: +
Assigned to: + + + + +
Keyword: + +
Priority: + +
Status: + + + + +
Resolution: +
No Sort or group:  
Pagesize:
Start With:
Sort Descending: +
Group Descending: +
Query name**: + + +
+   + +
  + + *: The "all text" field will look in message bodies and task titles +
+ + **: If you supply a name, the query will be saved off and available as a + link in the sidebar + +
+ +
+ + +
diff --git a/share/roundup/templates/devel/html/tracker.css b/share/roundup/templates/devel/html/tracker.css new file mode 100644 index 0000000..3cce4af --- /dev/null +++ b/share/roundup/templates/devel/html/tracker.css @@ -0,0 +1,20 @@ +#search { text-align: right; } + +p.ok-message +{ + background-color: #eeffee; + border: 1px solid #009900; + padding: 5px; + color: #009900; + font-weight: bold; +} +p.error-message +{ + background-color: #ffeeee; + border: 1px solid #990000; + padding: 5px; + color: #990000; + font-weight: bold; +} + +tr.odd { background-color:#f5f5f5; } diff --git a/share/roundup/templates/devel/html/user.forgotten.html b/share/roundup/templates/devel/html/user.forgotten.html new file mode 100644 index 0000000..cb8171f --- /dev/null +++ b/share/roundup/templates/devel/html/user.forgotten.html @@ -0,0 +1,56 @@ + +Password reset request - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +Password reset request + + + + +

You have two options if you have forgotten your password. +If you know the email address you registered with, enter it below.

+ +

If your user was automatically created during +import from the old sourceforge tracker, your e-mail address is +<Sourceforge username>@users.sourceforge.net. The mail address +associated with your account can be changed after login.

+ +
+ + + + + + + + + +
Email Address:
  + + + +
+ +

Or, if you know your username, then enter it below.

+ +

If you have previously created or modified issue +reports in the sourceforge issue tracker, you have an account here with +the same username as your sourceforge username.

+ + + + +
Username:
+
+ +

A confirmation email will be sent to you - +please follow the instructions within it to complete the reset process.

+ +
+ + +g +
diff --git a/share/roundup/templates/devel/html/user.help-search.html b/share/roundup/templates/devel/html/user.help-search.html new file mode 100644 index 0000000..e32a469 --- /dev/null +++ b/share/roundup/templates/devel/html/user.help-search.html @@ -0,0 +1,85 @@ + + + Search input for user helper + + + + + +
+    
+ + + + + + + + + + + + + + + + + + + + + + + + +
Name
Phone
role + +
  + + + + + + +
+ +
+
+
+  
+
diff --git a/share/roundup/templates/devel/html/user.help.html b/share/roundup/templates/devel/html/user.help.html
new file mode 100644
index 0000000..328d789
--- /dev/null
+++ b/share/roundup/templates/devel/html/user.help.html
@@ -0,0 +1,118 @@
+
+
+  
+     
+    
+    
+  
+  
+    
+
+ + + + + + + + + + + + + + + + + + + + +
Name
Phone
role + +
  + + + + + + +
+
+ +
+ +

Please specify your search parameters!

+
+
+
+ +
+ + + + +
+ +
+
+
+
diff --git a/share/roundup/templates/devel/html/user.index.html b/share/roundup/templates/devel/html/user.index.html new file mode 100644 index 0000000..ed9c3ef --- /dev/null +++ b/share/roundup/templates/devel/html/user.index.html @@ -0,0 +1,103 @@ + +User listing - <span + i18n:name="tracker" tal:replace="config/TRACKER_NAME" /> +User listing + + +You are not allowed to view this page. + +Please login with your username and password. + +
+ + + + + + + + + + + + + +
Search for users
Username + + Realname + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
UsernameReal nameOrganisationEmail addressPhone numberRetire
+ username +      + retire +
+ + + + + + +
+
+ + + + +
diff --git a/share/roundup/templates/devel/html/user.item.html b/share/roundup/templates/devel/html/user.item.html new file mode 100644 index 0000000..375aac7 --- /dev/null +++ b/share/roundup/templates/devel/html/user.item.html @@ -0,0 +1,165 @@ + + +<tal:if condition="context/id" i18n:translate="" + >User <span tal:replace="context/id" i18n:name="id" + />: <span tal:replace="context/username" i18n:name="title" + /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:if> +<tal:if condition="not:context/id" i18n:translate="" + >New User - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker" +/></tal:if> + + + + + + + New User + New User Editing + User + User Editing + + + + +

+ You are not allowed to view this page.

+ +

+ Please login with your username and password.

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name
Login Name
Login Password
Confirm Password
+ + + + + + + (to give the user more than one role, + enter a comma,separated,list) +
Phone
Organisation
Timezone +
E-mail address + calvin@the-z.org + + + +   +
+ +
+   + + + + +
+
+ + + + + + + + +
Note: highlighted fields are required.
+
+ + + +
+ + + +
diff --git a/share/roundup/templates/devel/html/user.register.html b/share/roundup/templates/devel/html/user.register.html new file mode 100644 index 0000000..0ee95b2 --- /dev/null +++ b/share/roundup/templates/devel/html/user.register.html @@ -0,0 +1,88 @@ + +Registering with <span i18n:name="tracker" + tal:replace="db/config/TRACKER_NAME" /> +Registering with + + +

NOTE: If you have previously created or modified issue +reports using the sourceforge issue tracker previously used for python +bugs, your username on sourceforge already exists in this tracker. Use +the Password recovery form to +get a password for your account.

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Namerealname
Login Nameusername
Login Passwordpassword
Confirm Passwordpassword
Rolesroles + +
Phonephone
Organisationorganisation
E-mail addressaddress
Alternate E-mail addresses
One address per line
alternate_addresses
  + + + + +
+
+ + + + + + + + +
Note: highlighted fields are required.
+
+ + + +
diff --git a/share/roundup/templates/devel/html/user.rego_progress.html b/share/roundup/templates/devel/html/user.rego_progress.html new file mode 100644 index 0000000..58e2f6c --- /dev/null +++ b/share/roundup/templates/devel/html/user.rego_progress.html @@ -0,0 +1,15 @@ + +Registration in progress - <span i18n:name="tracker" + tal:replace="config/TRACKER_NAME" /> +Registration in progress... + + +

You will shortly receive an email +to confirm your registration. To complete the registration process, +visit the link indicated in the email. +

+ + +
diff --git a/share/roundup/templates/devel/html/user_utils.js b/share/roundup/templates/devel/html/user_utils.js new file mode 100644 index 0000000..b992f6d --- /dev/null +++ b/share/roundup/templates/devel/html/user_utils.js @@ -0,0 +1,111 @@ +// User Editing Utilities + +/** + * for new users: + * Depending on the input field which calls it, takes the value + * and dispatches it to certain other input fields: + * + * address + * +-> username + * | `-> realname + * `-> organisation + */ +function split_name(that) { + var raw = that.value + var val = trim(raw) + if (val == '') { + return + } + var username='' + var realname='' + var address='' + switch (that.name) { + case 'address': + address=val + break + case 'username': + username=val + break + case 'realname': + realname=val + break + default: + alert('Ooops - unknown name field '+that.name+'!') + return + } + var the_form = that.form; + + function field_empty(name) { + return the_form[name].value == '' + } + + // no break statements - on purpose! + switch (that.name) { + case 'address': + var split1 = address.split('@') + if (field_empty('username')) { + username = split1[0] + the_form.username.value = username + } + if (field_empty('organisation')) { + the_form.organisation.value = default_organisation(split1[1]) + } + case 'username': + if (field_empty('realname')) { + realname = Cap(username.split('.').join(' ')) + the_form.realname.value = realname + } + case 'realname': + if (field_empty('username')) { + username = Cap(realname.replace(' ', '.')) + the_form.username.value = username + } + if (the_form.firstname && the_form.lastname) { + var split2 = realname.split(' ') + var firstname='', lastname='' + firstname = split2[0] + lastname = split2.slice(1).join(' ') + if (field_empty('firstname')) { + the_form.firstname.value = firstname + } + if (field_empty('lastname')) { + the_form.lastname.value = lastname + } + } + } +} + +function SubCap(str) { + switch (str) { + case 'de': case 'do': case 'da': + case 'du': case 'von': + return str; + } + if (str.toLowerCase().slice(0,2) == 'mc') { + return 'Mc'+str.slice(2,3).toUpperCase()+str.slice(3).toLowerCase() + } + return str.slice(0,1).toUpperCase()+str.slice(1).toLowerCase() +} + +function Cap(str) { + var liz = str.split(' ') + for (var i=0; i/db/files/] +# type = String() [MIME type of the content, default 'text/plain'] +msg = FileClass(db, "msg", + author=Link("user", do_journal='no'), + recipients=Multilink("user", do_journal='no'), + date=Date(), + summary=String(), + files=Multilink("file"), + messageid=String(), + inreplyto=String(), + revision=Link("vcs_rev")) + +# File +file = FileClass(db, "file", + name=String(), + description=String(indexme='yes')) + +# Patch +patch = FileClass(db, "patch", + name=String(), + description=String(indexme='yes'), + repository=String(), + revision=String()) + +# Bug Type +bug_type = Class(db, 'bug_type', + name=String(), + description=String(), + order=Number()) +bug_type.setkey('name') + +# IssueClass automatically gets these properties in addition to the Class ones: +# title = String() +# messages = Multilink("msg") +# files = Multilink("file") +# patches = Multilink("patches") +# nosy = Multilink("user") +# superseder = Multilink("issue") +bug = IssueClass(db, "bug", + type=Link('bug_type'), + components=Multilink('component'), + versions=Multilink('version'), + severity=Link('severity'), + priority=Link('priority'), + dependencies=Multilink('bug'), + assignee=Link('user'), + status=Link('status'), + resolution=Link('resolution'), + superseder=Link('bug'), + keywords=Multilink('keyword')) + +# Task Type +task_type = Class(db, 'task_type', + name=String(), + description=String(), + order=Number()) +task_type.setkey('name') + +# IssueClass automatically gets these properties in addition to the Class ones: +# title = String() +# messages = Multilink("msg") +# files = Multilink("file") +# nosy = Multilink("user") +# superseder = Multilink("issue") +task = IssueClass(db, "task", + type=Link('task_type'), + components=Multilink('component'), + priority=Link('priority'), + dependencies=Multilink('task'), + assignee=Multilink('user'), + status=Link('status'), + resolution=Link('resolution'), + solves=Link('bug')) + +milestone = IssueClass(db, "milestone", + bugs=Multilink("bug"), + tasks=Multilink("task"), + status=Link("status"), + release_date=String()) + +# +# TRACKER SECURITY SETTINGS +# +# See the configuration and customisation document for information +# about security setup. + +db.security.addRole(name='Developer', description='A developer') +db.security.addRole(name='Coordinator', description='A coordinator') + +# +# REGULAR USERS +# +# Give the regular users access to the web and email interface +for r in 'User', 'Developer', 'Coordinator': + db.security.addPermissionToRole(r, 'Web Access') + db.security.addPermissionToRole(r, 'Email Access') + +########################## +# User permissions +########################## + +for cl in ('severity', 'component', + 'version', 'priority', 'status', 'resolution', + 'bug_type', 'bug', 'task_type', 'task', 'milestone', + 'keyword', 'file', 'msg'): + db.security.addPermissionToRole('User', 'View', cl) + db.security.addPermissionToRole('Anonymous', 'View', cl) + db.security.addPermissionToRole('User', 'Create', cl) + + +def may_edit_file(db, userid, itemid): + return userid == db.file.get(itemid, "creator") + +p = db.security.addPermission(name='Edit', klass='file', check=may_edit_file, + description="User is allowed to remove their own files") +db.security.addPermissionToRole('User', p) + +p = db.security.addPermission(name='Create', klass='bug', + properties=('title', 'bug_type', + 'components', 'versions', + 'severity', + 'messages', 'files', 'nosy'), + description='User can report and discuss bugs') +db.security.addPermissionToRole('User', p) + +p = db.security.addPermission(name='Edit', klass='bug', + properties=('title', 'bug_type', + 'components', 'versions', + 'severity', + 'messages', 'files', 'nosy'), + description='User can report and discuss bugs') +db.security.addPermissionToRole('User', p) + +p = db.security.addPermission(name='Create', klass='task', + properties=('title', 'task_type', + 'components', + 'messages', 'files', 'nosy'), + description='Developer can create and discuss tasks') +db.security.addPermissionToRole('Developer', p) + +p = db.security.addPermission(name='Edit', klass='task', + properties=('title', 'task_type', + 'components', + 'messages', 'files', 'nosy'), + description='Developer can create and discuss tasks') +db.security.addPermissionToRole('Developer', p) + +p = db.security.addPermission(name='Create', klass='milestone', + description='Coordinator can create and discuss milestones') +db.security.addPermissionToRole('Coordinator', p) + +p = db.security.addPermission(name='Edit', klass='milestone', + description='Coordinator can create and discuss milestones') +db.security.addPermissionToRole('Coordinator', p) + + +########################## +# Developer permissions +########################## +for cl in ('bug_type', 'severity', 'component', + 'version', 'priority', 'status', 'resolution', + 'bug', 'file', 'msg', 'keyword'): + db.security.addPermissionToRole('Developer', 'View', cl) + +for cl in ('bug', 'file', 'msg', 'keyword'): + db.security.addPermissionToRole('Developer', 'Edit', cl) + db.security.addPermissionToRole('Developer', 'Create', cl) + + +########################## +# Coordinator permissions +########################## +for cl in ('bug_type', 'task_type', 'severity', 'component', + 'version', 'priority', 'status', 'resolution', 'bug', 'task', 'file', 'msg'): + db.security.addPermissionToRole('Coordinator', 'View', cl) + db.security.addPermissionToRole('Coordinator', 'Edit', cl) + db.security.addPermissionToRole('Coordinator', 'Create', cl) + +# May users view other user information? Comment these lines out +# if you don't want them to +db.security.addPermissionToRole('User', 'View', 'user') +db.security.addPermissionToRole('Developer', 'View', 'user') +db.security.addPermissionToRole('Coordinator', 'View', 'user') + +# Allow Coordinator to edit any user, including their roles. +db.security.addPermissionToRole('Coordinator', 'Edit', 'user') +db.security.addPermissionToRole('Coordinator', 'Web Roles') + +# Users should be able to edit their own details -- this permission is +# limited to only the situation where the Viewed or Edited item is their own. +def own_record(db, userid, itemid): + '''Determine whether the userid matches the item being accessed.''' + return userid == itemid +p = db.security.addPermission(name='View', klass='user', check=own_record, + description="User is allowed to view their own user details") +for r in 'User', 'Developer', 'Coordinator': + db.security.addPermissionToRole(r, p) +p = db.security.addPermission(name='Edit', klass='user', check=own_record, + description="User is allowed to edit their own user details", + properties=('username', 'password', + 'address', 'realname', + 'phone', 'organization', + 'alternate_addresses', + 'queries', + 'timezone')) # Note: 'roles' excluded - users should not be able to edit their own roles. +for r in 'User', 'Developer': + db.security.addPermissionToRole(r, p) + +# Users should be able to edit and view their own queries. They should also +# be able to view any marked as not private. They should not be able to +# edit others' queries, even if they're not private +def view_query(db, userid, itemid): + private_for = db.query.get(itemid, 'private_for') + if not private_for: return True + return userid == private_for +def edit_query(db, userid, itemid): + return userid == db.query.get(itemid, 'creator') +p = db.security.addPermission(name='View', klass='query', check=view_query, + description="User is allowed to view their own and public queries") +for r in 'User', 'Developer', 'Coordinator': + db.security.addPermissionToRole(r, p) +p = db.security.addPermission(name='Edit', klass='query', check=edit_query, + description="User is allowed to edit their queries") +for r in 'User', 'Developer', 'Coordinator': + db.security.addPermissionToRole(r, p) +p = db.security.addPermission(name='Create', klass='query', + description="User is allowed to create queries") +for r in 'User', 'Developer', 'Coordinator': + db.security.addPermissionToRole(r, p) + + +# +# ANONYMOUS USER PERMISSIONS +# +# Let anonymous users access the web interface. Note that almost all +# trackers will need this Permission. The only situation where it's not +# required is in a tracker that uses an HTTP Basic Authenticated front-end. +db.security.addPermissionToRole('Anonymous', 'Web Access') + +# Let anonymous users access the email interface (note that this implies +# that they will be registered automatically, hence they will need the +# "Create" user Permission below) +# This is disabled by default to stop spam from auto-registering users on +# public trackers. +#db.security.addPermissionToRole('Anonymous', 'Email Access') + +# Assign the appropriate permissions to the anonymous user's Anonymous +# Role. Choices here are: +# - Allow anonymous users to register +db.security.addPermissionToRole('Anonymous', 'Create', 'user') + +# Allow anonymous users access to view issues (and the related, linked +# information). + +for cl in 'bug', 'task', 'milestone', 'severity', 'status', 'resolution', 'msg', 'file': + db.security.addPermissionToRole('Anonymous', 'View', cl) + +# [OPTIONAL] +# Allow anonymous users access to create or edit "issue" items (and the +# related file and message items) +#for cl in 'issue', 'file', 'msg': +# db.security.addPermissionToRole('Anonymous', 'Create', cl) +# db.security.addPermissionToRole('Anonymous', 'Edit', cl) + + +# vim: set filetype=python sts=4 sw=4 et si : +