## Script (Python) "form_validator" ##bind container=container ##bind context=context ##bind namespace= ##bind script=script ##bind subpath=traverse_subpath ##parameters= ##title=Generates Squishdot postings and polls from form submissions ## from Products.Formulator.Errors import ValidationError, FormValidationError from Products.PythonScripts.standard import DTML ########################################################################### # Information # ########################################################################### # This script requires that certain fields be present within the form that # it is called by. These fields are: # 1) formName: The name of the Formulator form object that the form # fields were generated from # # 2) folder: The relative path to the Squishdot site that the generated # posting should be put in # # 3) aFolder: The relative path to the Archives folder that this # request's data should be stored in # # # This script uses other fields, but they are not necessarily required: # 1) recipient_roles: User roles that should receive notification of # new submissions. If this is left out, nobody # will receive notification when a form is # submitted. # # 2) email: The email address of the person submitting the form. If # this is left out, the email address will be changed to # 'Unknown_Email' # # 3) realname: The name of the person submitting the form. If this is # left out, the name of the person submitting the form # will be changed to 'Anonymous' # # 4) subject: The subject to use for the email message. If this is # left out, the subject will be changed to 'Form Submission' # # 5) poll: Whether or not you would like to make a poll for this post # (1=Make poll, anything else (or omitted)=Do not make poll) # # 6) question: The question to be asked in the poll. If this is left # out, the question will just display as 'Poll' # # 7) responses: The various answers that a user can give for a poll. # This can contain as many as you want, but be aware that # the Squishdot layout will become cluttered if you use # more than three of a general length. Each response # option should be separated by a comma (,). Responses # will be displayed within the Poll in the order that # they are provided in this field. If this is left out, # the poll will not be created. (Kinda hard to have a # poll that does not have any response choices...) # ########################################################################### # Config area # ########################################################################### # The max width of a form field's contents when sent as an email message maxWidth = 65 # Field names that are reserved for use by the script, and should not # be displayed in the generated Squishdot post. DO NOT change this unless # you properly modify the appropriate parts of the script config_fields = ['formName','folder','aFolder','recipient_roles','poll', 'subject','question','responses'] ########################################################################### # Start of actual script # ########################################################################### # Get the value of the Formulator object to use for validation. This is # based on the value of the formName field. This field should have the # same value as the name specified in the
tag that # starts a request form, which should be the same name as the ID of the # Formulator object you get all of the form elements from. if (context.REQUEST.form.has_key('formName')): form = context[context.REQUEST.form.get('formName')] else: raise AttributeError, "You must provide the name of the Formulator validation form in the 'formName' field." # Get the value of the folder field. This field should have the value that # represents the name of the folder where discussion articles will be # created. (The location of the Squishdot site.) If the folder is not on # the same level as this script, you can enter the relative path for the # folder. DO NOT enter a full URL. # # Examples: # 1. form_validator script at /activities/form_validator # and Squishdot site at /activities/operations/discussion: # * Use 'operations/discussion' as the value of the 'folder' field # # 2. form_validator script at /activities/operations/form_validator # and Squishdot site at /discussion: # * Use '../../discussion' as the value of the 'folder' field # # You get the idea. if (context.REQUEST.form.has_key('folder')): folder = context.REQUEST.form.get('folder') else: folder = '' if (context.REQUEST.form.has_key('aFolder')): aFolder = context.REQUEST.form.get('aFolder') else: aFolder = '' # Set up the values used for redirecting to the original form # The query string containing all field values returnVals = '' # The "Description" property values for each field myFieldDescriptions = [] # The ID property values for each field myFieldIDs = [] # Request data, stored in a format that is easy to parse (used # for the archived requests and writing data to the filesystem # when a request is approved.) requestData = [] # Loop through each field in the form for field in form.get_fields(): # If it is not a configuration field if not field.id in config_fields: # Try to get the description and store it. If it can't # be found, use the field ID instead if (field.get_value('description') != ''): desc = field.get_value('description') else: desc = field.id # If the description ends with ':', cut off the last # character. (':' is automatically appended later) if (desc[-1] == ':'): desc = desc[:-1] # Put the description and ID into their respective lists myFieldDescriptions.append(desc) myFieldIDs.append(field.id) # Try to get the value of the field and store it. If it # can't be found, store '' as the field value if (getattr(context.REQUEST, 'field_' + field.id) != None): val = getattr(context.REQUEST, 'field_' + field.id) # If the value is a list, put commas between the values and turn # them into a single value if (same_type(val,[])): fval = '' for sub_val in val: fval = fval + sub_val + ', ' if (len(fval) > 1): val = fval[:-2] else: val = fval else: val = '' # Append new information for this field and its value to the # query string, and append other field information to the # requestData list returnVals = returnVals + field.id + '=' + val + '&' requestData.append(field.id + '||' + desc + '||' + val) # Find and store the fully-qualified URL of the referring page, minus # a query string (if one exists) previous_page = (getattr(context.REQUEST, 'HTTP_REFERER')).split('?')[0] # Attempt to validate the form's input. If it does not pass, append the # error messages and their fields to the return query, make the form # data URL-legal, and redirect back to the original form try: result = form.validate_all(context.REQUEST) except FormValidationError, errlist: for error in errlist.errors: returnVals = returnVals + error.field.id + '_errMsg=' + error.error_text + '&' # Replace spaces with '%20', returns with '%0D', and newlines with '%0A' # so that the form data can be stored in the query string and returned returnVals = returnVals.replace(" ", "%20") returnVals = returnVals.replace("\r", "%0D") returnVals = returnVals.replace("\n", "%0A") return context.REQUEST.RESPONSE.redirect(previous_page + '?success=0&' + returnVals) # If the form data passes validation, start generating stuff and mailing information if result: # Attempt to locate the Plone Mail Host try: mailhost=getattr(context, context.superValues('Mail Host')[0].id) except: raise AttributeError, "Can't find a Mail Host object." # Start making the email that gets sent out when someone submits the form # Email address only if result.has_key('email'): emailAddr = result['email'] else: emailAddr = 'Unknown_Email' # Email address with @ symbol converted to URL-safe (a) emailAddrC = string.replace(emailAddr, "@", "(a)") # Name only if result.has_key('realname'): personName = result['realname'] else: personName = 'Anonymous' # The subject for the email message... This is retrieved from # form itself, stored in a hidden field if result.has_key('subject'): mSubj = result['subject'] else: mSubj = 'Form submission' # Initialize the message body, article body, and the document contents preMailMsg = [] preDiscMsg = [] allData = '' # Formatted date, used for part of the generated document ID dateString = '' + str(DateTime().month()) + '-' + str(DateTime().day()) + '-' + str(DateTime().year()) + '' # Formatted time, used for part of the generated document title formattedTime = str(DateTime().strftime('%H:%M:%S')) # Get the archives folder path, split it up, and move to the proper folder id = '' aFolder = aFolder.strip() aFolder = aFolder.strip('/') if (aFolder != ''): pathParts = aFolder.split('/') archive_Container = container for part in pathParts: if (part[0] != '.'): # If for some reason one of the folders on the way to the # destination folder is missing, create it if (part not in archive_Container.objectIds()): archive_Container.manage_addProduct['CMFPlone'].manage_addContent(type='Plone Folder', id=part) archive_Container = archive_Container[part] else: if (part == '.'): archive_Container = archive_Container if (part == '..'): archive_Container = archive_Container.aq_parent # Generate a unique ID for this request archive entry, which will also be used for # the Poll ID, unless a Poll is not being created. id = '' + str(len(archive_Container.objectIds())+1) + '_' + emailAddrC + '_' + dateString + '' # Now we have the container for the Archived posts stored in # "archive_Container," and have a unique ID for the request/poll # Get the folder path, split it up, and move to the proper Squishdot folder folder = folder.strip() folder = folder.strip('/') if (folder != ''): pathParts = folder.split('/') container2 = container for part in pathParts: if (part[0] != '.'): # Here we don't want to create a Squishdot site on our own, since they # require a bit of input from the administrator... So fail if this path # does not exist. try: container2 = container2[part] except: raise AttributeError, "The specified path (" + folder + ") does not exist." else: if (part == '.'): container2 = container2 if (part == '..'): container2 = container2.aq_parent # At this point, container2 should be the proper folder for the Squishdot postings # Try to retrieve values for a poll, and create it if possible if (context.REQUEST.form.has_key('poll')): poll = context.REQUEST.form.get('poll') else: poll = '' if (context.REQUEST.form.has_key('question')): question = context.REQUEST.form.get('question') question = question.strip() if (len(question) < 1): question = 'Poll' else: question = 'Poll' if (context.REQUEST.form.has_key('responses')): responses = context.REQUEST.form.get('responses') else: responses = '' # If we need to create a poll for this submission if ((poll == '1') and (responses != '')): # If for some reason the Polls folder is missing, create it if ('Polls' not in container2.objectIds()): container2.manage_addProduct['CMFPlone'].manage_addContent(type='Plone Folder', id='Polls') # Move to the Polls folder container3 = container2.Polls # Create the poll ID (always unique) if an ID was not already made created by the # Archives creation if (id == ''): id = '' + str(len(container3.objectIds())+1) + '_' + emailAddrC + '_' + dateString + '' # Remove leading/trailing whitespace from provided responses, split them by commas, # and then strip leading/trailing whitespace from each split value and put it into # another list if its length is greater than zero responses = responses.strip() responses = responses.strip(',') resps = responses.split(',') finalResps = [] for resp in resps: resp = resp.strip() if (len(resp) > 0): finalResps.append(resp) # If there are responses for this poll: if (len(finalResps) > 0): # Create a new FSPoll object with the generated ID container3.manage_addProduct['FSPoll'].manage_addFSPoll(id=id, title="", REQUEST=context.REQUEST) # Set the "Approved" property for the Poll, which links to teh article doc=getattr(container3, id) doc.manage_addProperty('Approved', 'false', 'string') # Move to the new FSPoll object container3 = container3[id] # Create a new FSPoll question with the proper ID and title container3.manage_addProduct['FSPoll'].manage_addFSPollQuestion(id="Question1", title=question, REQUEST=context.REQUEST) # Move to the new FSPoll question object container3 = container3['Question1'] # Loop through the list of responses and make a new FSPollAnswer object # for each of them. for resp in finalResps: container3.manage_addProduct['FSPoll'].manage_addFSPollAnswer(title=resp, REQUEST=context.REQUEST) # Generate the title of the document that is going to be created titleText2 = mSubj + ' made by ' + personName + ' (' + emailAddr + ') on ' + str(DateTime().Day()) + ', ' + str(DateTime().Month()) + ' ' + str(DateTime().day()) + ', ' + str(DateTime().year()) + ' at ' + formattedTime + '' titleText = mSubj + ' made by ' + personName + ' (' + emailAddr + ') on ' + str(DateTime().Day()) + ', ' + str(DateTime().Month()) + ' ' + str(DateTime().day()) + ', ' + str(DateTime().year()) + ' at ' + formattedTime + '' # Append the first few lines of the message for both the email and the Squishdot posting preMailMsg.append("The following was submitted by: %s (%s)" % (result['realname'], result['email'])) preDiscMsg.append("The following was submitted by: %s (\"mailto:%s\">%s)" % (result['realname'], result['email'], result['email'])) preMailMsg.append(" on " + str(DateTime().Day()) + ", " + str(DateTime().Month()) + " " + str(DateTime().day()) + ", " + str(DateTime().year()) + " at " + formattedTime) preDiscMsg.append(" on " + str(DateTime().Day()) + ", " + str(DateTime().Month()) + " " + str(DateTime().day()) + ", " + str(DateTime().year()) + " at " + formattedTime) # Get rid of '..' and '.' in the resulting URL (I didn't like them!), # and replace them with the correct path URL1 = getattr(context.REQUEST, 'URL1').strip() URL1 = URL1.strip('/') if (URL1 != ''): URLpathParts = URL1.split('/') for part in pathParts: if (part[0] != '.'): URLpathParts.append(part) elif (part == '..'): URLpathParts = URLpathParts[:-1] dURLStart = '' for urlPart in URLpathParts: dURLStart = dURLStart + urlPart + '/' # Store the URL for the main discussion site corresponding to this request dURL = dURLStart # Store the URL for the main discussion site corresponding to this request (for the email message only!) dURL2 = dURLStart + '[articleID]' dURLHTML = dURLStart.replace('/' + folder, '
/' + folder) # Append more lines of the message for the email and the Squishdot posting preMailMsg.append("\nThis submission has been setup for discussion at:") preDiscMsg.append("\nThis submission has been setup for discussion at:") preMailMsg.append(" " + dURL2 + "/") preDiscMsg.append(" \"%s\">%s" % (dURL, dURLHTML)) preMailMsg.append("-------------------------------------------------------------------\n") preDiscMsg.append("-------------------------------------------------------------------\n") # Loop through all of the form fields and write their data to the email # message and the Squishdot posting... Keep all lines within the # specified line width by splitting them on newlines and spaces between # a certain point and the maximum width... Include tabs in all lines to # make the email easier to read i = 0 for key in myFieldIDs: if (result[key] and (key not in config_fields)): data = '\t' lineWidth = 0 # If the result is a list, put commas between the values and turn # them into a single value before it gets put into the email message if (same_type(result[key], [])): fval = '' for sub_val in result[key]: fval = fval + sub_val + ', ' if (len(fval) > 1): val = fval[:-2] else: val = fval result[key] = val # Examine each character in the data for curChar in result[key]: # If it is a space between maxWidth-15 and maxWidth characters, # or it is a newline, replace it with a newline and a tab and # reset the line width to 0 if ((curChar == ' ' and (lineWidth <= maxWidth and (lineWidth >= (maxWidth -15)))) or (curChar == '\n')): data = data + '\n\t' lineWidth = 0 # Otherwise, just store it in the data string and increment the line width else: data = data + curChar lineWidth = lineWidth + 1 # Write the field description and the modified data to the email message and the article preMailMsg.append("%s:\n%s\n" % (myFieldDescriptions[i], data)) preDiscMsg.append("%s:\n%s\n" % (myFieldDescriptions[i], data)) allData = allData + '\n' + myFieldDescriptions[i] + ':\n' + data + '\n' # Ignore the fields that have the user's name and email address, since # that data was already included elif (key == 'realname' or key == 'email'): allData = allData + '\n' + myFieldDescriptions[i] + ':\n\t' + result[key] + '\n' elif (key == 'subject'): allData = allData # If the field data could not be found, just append "Not Provided" to # the message else: preMailMsg.append("%s:\n\tNot provided\n" % myFieldDescriptions[i]) preDiscMsg.append("%s:\n\tNot provided\n" % myFieldDescriptions[i]) allData = allData + '\n' + myFieldDescriptions[i] + ':\n\tNot provided\n' # Increment the counter i = i + 1 # Write the last few lines of the message, which include the REMOTE_ADDR and REMOTE_HOST preMailMsg.append("-------------------------------------------------------------------\n") preDiscMsg.append("-------------------------------------------------------------------\n") preMailMsg.append("REMOTE_HOST: " + getattr(context.REQUEST, getattr(context.REQUEST, 'REMOTE_HOST', None), 'Unresolved')) preDiscMsg.append("REMOTE_HOST: " + getattr(context.REQUEST, getattr(context.REQUEST, 'REMOTE_HOST', None), 'Unresolved')) preMailMsg.append("REMOTE_ADDR: " + getattr(context.REQUEST, 'HTTP_X_FORWARDED_FOR', getattr(context.REQUEST, 'REMOTE_ADDR', None))) preDiscMsg.append("REMOTE_ADDR: " + getattr(context.REQUEST, 'HTTP_X_FORWARDED_FOR', getattr(context.REQUEST, 'REMOTE_ADDR', None))) # Put a newline at the beginning of the message and the article and store it all mailMsg = '\n'.join(preMailMsg) discMsg = '\n'.join(preDiscMsg) if (aFolder != ''): # Create the new document with the given id, title, and data, # and store it in the Archives/folder folder. archive_Container.manage_addProduct['OFSP'].manage_addDTMLDocument(id, title=titleText2, file=allData) # Retrieve the document and set some properties for it to make processing # documents and sending out renewal requests easier doc=getattr(archive_Container, id) doc.manage_addProperty('requestor_name', result['realname'], 'string') doc.manage_addProperty('requestor_email', result['email'], 'string') doc.manage_addProperty('request_date', dateString, 'string') doc.manage_addProperty('request_data', requestData, 'lines') doc.manage_addProperty('description', titleText, 'string') doc.manage_addProperty('Approved', 'No', 'string') # Replace spaces with '%20', returns with '%0D', and newlines with '%0A' # so that the form data can be stored in the query string and returned returnVals = string.replace(returnVals, " ", "%20") returnVals = string.replace(returnVals, "\r", "%0D") returnVals = string.replace(returnVals, "\n", "%0A") # Start converting values in the Squishdot posting body to make it more # web browser friendly discMsg = discMsg.replace("\r", "
") discMsg = discMsg.replace("\n", "
") discMsg = discMsg.replace("\t", "     ") discMsg = discMsg.replace(" ", "     ") discMsg = discMsg.replace(" ", "    ") discMsg = discMsg.replace(" ", "   ") discMsg = discMsg.replace(" ", "  ") discMsg = discMsg.replace("", "') dSubj = DTML('') formatFrom = dFrom(container, context.REQUEST) formatSubj = dSubj(container, context.REQUEST) formatSubj = '[' + formatSubj + ' Plone Portal] ' + mSubj # If there is a valid requestor email address: if (emailAddr != 'Unknown_Email'): # Send a copy of the request to the requestor, along with the article ID so that they reqMailMsg = [] reqMailMsg.append("%s," % personName) reqMailMsg.append("Thank you for your %s. A copy of your submission has been included below." % (mSubj)) reqMailMsg.append("To view the discussion of your submission, visit %s" %(string.replace(dURL2, "[articleID]", articleID))) reqMailMsg.append("") reqMailMsg.append("Please do not reply to this message.") reqMailMsg.append("\n") reqMailMsg.append(allData) reqMailMsg = '\n'.join(reqMailMsg) mailhost.simple_send(mto=result['email'], mfrom=formatFrom, subject='Your ' + mSubj, body=reqMailMsg) # Send the message to the list of members who should receive it, based on role # Go through the list of roles that should receive an email when a form is submitted, # and send the email to the users of the role. if (context.REQUEST.form.has_key('recipient_roles')): # Create an empty list that will be used to store the user names of members who have # already received the status update email sentTo = [] # First replace the [articleID] marker in the email message with the actual # article ID, since the article has already been posted and we now know the ID. mailMsg = string.replace(mailMsg, "[articleID]", articleID) # Get the roles into a list role_val = context.REQUEST.form.get('recipient_roles') role_val = role_val.strip() role_val = role_val.strip(',') roles = role_val.split(',') finalRoles = [] # Go through each of the roles and strip them of leading/trailing whitespace, # and only append them to the finalRoles list if they have a length of more # than 0 for role in roles: role = role.strip() if (len(role) > 0): finalRoles.append(role) doc.manage_addProperty('recipients', finalRoles, 'lines') # If there are user roles to send this message to: if (len(finalRoles) > 0): for item in context.GCSC.portal_membership.listMembers(): memberId = item.id try: for r in finalRoles: if (r in context.GCSC.portal_membership.getMemberById(memberId).getRoles() and (item.email !='') and (memberId not in sentTo)): mTo = item.email mailhost.simple_send(mto=mTo, mfrom='"' + personName + '" <' + emailAddr + '>', subject=formatSubj, body=mailMsg) sentTo.append(memberId) except: roleFailError=1 # Redirect back to the page that called the script, with the success value set to 1 so that # the form knows to display the success message context.REQUEST.RESPONSE.redirect(previous_page + '?success=1&' + returnVals)