def post(self): try: # verify user can write the data, otherwise abort (adapted from loutilities.tables._editormethod) if not self.permission(): db.session.rollback() cause = 'operation not permitted for user' return jsonify(error=cause) # there should be one 'id' in this form data, 'keyless' requestdata = get_request_data(request.form) motion_id = request.args['motion_id'] from_email = requestdata['keyless']['from_email'] subject = requestdata['keyless']['subject'] message = requestdata['keyless']['message'] generateevotes(motion_id, from_email, subject, message) self._responsedata = [] db.session.commit() return jsonify(self._responsedata) except Exception as e: exc = ''.join(format_exception_only(type(e), e)) output_result = {'status' : 'fail', 'error': 'exception occurred:<br>{}'.format(exc)} # roll back database updates and close transaction db.session.rollback() current_app.logger.error(format_exc()) return jsonify(output_result)
def _validate(self, action, formdata): results = [] # kludge to get task.id # NOTE: this is only called from 'edit' / put function, and there will be only one id thisid = list(get_request_data(request.form).keys())[0] thistask = Task.query.filter_by(id=thisid).one() # build lists of required and shared fields required = [] one_of = [] override_completion = [] for tasktaskfield in thistask.fields: taskfield = tasktaskfield.taskfield # ignore display-only fields if taskfield.inputtype == INPUT_TYPE_DISPLAY: continue if tasktaskfield.need == NEED_REQUIRED: required.append(taskfield.fieldname) elif tasktaskfield.need == NEED_ONE_OF: one_of.append(taskfield.fieldname) if taskfield.override_completion: override_completion.append(taskfield.fieldname) # verify required fields were supplied for field in required: if not formdata[field]: results.append({'name': field, 'status': 'please supply'}) # verify one of the one_of fields was supplied onefound = False for field in one_of: if formdata[field]: onefound = True if not onefound: for field in one_of: results.append({'name':field, 'status': 'one of these must be supplied'}) # verify fields which override completion date (should only be one if configured properly) for field in override_completion: if formdata[field] > date.today().isoformat(): results.append({'name':field, 'status': 'cannot specify date later than today'}) return results
def editor_method_posthook(self, form): #---------------------------------------------------------------------- ''' send contract to client contact if asked to do so, after processing put() note row has already been committed to the database, so can be retrieved ''' # the following can be true only for put() [edit] method if 'addlaction' in form and form['addlaction'] in [ 'sendcontract', 'resendcontract' ]: folderid = current_app.config['CONTRACTS_DB_FOLDER'] # need an instance of contract manager to take care of saving the contract cm = ContractManager(contractType='race services', templateType='contract', driveFolderId=folderid) # pull record(s) from database and save as flat dotted record data = get_request_data(form) print(('data={}'.format(data))) for thisid in data: eventdb = Event.query.filter_by(id=thisid).one() # different subject line if contract had been accepted before. This must match contractviews.AcceptAgreement.post annotation = '' # if we are generating a new version of the contract if form['addlaction'] == 'sendcontract': # if there was already a document sent, indicate that we're updating it if eventdb.contractDocId: eventdb.isContractUpdated = True annotation = '(updated) ' # check appropriate fields are present for certain services servicenames = {s.service for s in eventdb.services} if servicenames & {'coursemarking', 'finishline'}: self._fielderrors = [] for field in [ 'race', 'date', 'mainStartTime', 'mainDistance' ]: if not data[thisid][field]: self._fielderrors.append({ 'name': field, 'status': 'please supply' }) ## handle select fields for field in [ 'state', 'services', 'client', 'course', 'lead' ]: if not data[thisid][field]['id']: self._fielderrors.append({ 'name': '{}.id'.format(field), 'status': 'please select' }) if self._fielderrors: raise parameterError('missing fields') # calculate service fees servicefees = [] feetotal = 0 for service in eventdb.services: servicefee = {'service': service.serviceLong} # fixed fee if service.feeType.feeType == 'fixed': thisfee = service.fee servicefee['fee'] = thisfee servicefees.append(servicefee) # fee is based on another field elif service.feeType.feeType == 'basedOnField': field = service.basedOnField # not clear why this needs to be converted to int, but otherwise see unicode value # if can't be converted, then invalid format try: fieldval = int(getattr(eventdb, field)) except (TypeError, ValueError) as e: fieldval = None # field not set, then set self._fielderrors appropriately if not fieldval: formfield = self.dbmapping[ field] # hopefully not a function self._fielderrors = [{ 'name': formfield, 'status': 'needed to calculate fee' }] raise parameterError( 'cannot calculate fee if {} not set'. format(field)) feebasedons = FeeBasedOn.query.filter_by( serviceId=service.id).order_by( FeeBasedOn.fieldValue).all() foundfee = False for feebasedon in feebasedons: lastfieldval = feebasedon.fieldValue if debug: current_app.logger.debug( 'fieldval={} feebasedon.fieldValue={}'. format(fieldval, feebasedon.fieldValue)) if debug: current_app.logger.debug( 'type(fieldval)={} type(feebasedon.fieldValue)={}' .format(type(fieldval), type(feebasedon.fieldValue))) if fieldval <= feebasedon.fieldValue: thisfee = feebasedon.fee servicefee['fee'] = thisfee servicefees.append(servicefee) foundfee = True break # if fee not found, then set fielderrors appropriately if not foundfee: formfield = self.dbmapping[ field] # hopefully not a function self._fielderrors = [{ 'name': formfield, 'status': 'cannot calculate fee if this is greater than {}' .format(lastfieldval) }] raise parameterError( 'cannot calculate fee if {} greater than {}' .format(field, lastfieldval)) # not sure how we could get here, but best to be defensive else: raise parameterError('unknown feeType: {}'.format( service.feeType.feeType)) # accumulate total fee feetotal += thisfee # need to calculate addons in addition to services for addon in eventdb.addOns: thisfee = addon.fee servicefee = { 'service': addon.longDescr, 'fee': thisfee } servicefees.append(servicefee) # accumulate total fee feetotal += thisfee # generate contract if debug: current_app.logger.debug( 'editor_method_posthook(): (before create()) eventdb.__dict__={}' .format(eventdb.__dict__)) docid = cm.create( '{}-{}-{}.docx'.format(eventdb.client.client, eventdb.race.race, eventdb.date), eventdb, addlfields={ 'servicenames': [s.service for s in eventdb.services], 'servicefees': servicefees, 'event': eventdb.race.race, 'totalfees': { 'service': 'TOTAL', 'fee': feetotal }, }) # update database to show contract sent eventdb.state = State.query.filter_by( state=STATE_CONTRACT_SENT).one() eventdb.contractSentDate = dt.dt2asc(date.today()) eventdb.contractDocId = docid # find index with correct id and show database updates for resprow in self._responsedata: if resprow['rowid'] == thisid: resprow['state'] = { key: val for (key, val ) in list(eventdb.state.__dict__.items()) if key[0] != '_' } resprow[ 'contractSentDate'] = eventdb.contractSentDate resprow['contractDocId'] = eventdb.contractDocId # if we are just resending current version of the contract else: docid = eventdb.contractDocId annotation = '(resend) ' # email sent depends on current state as this flows from 'sendcontract' and 'resendcontract' if eventdb.state.state == STATE_COMMITTED: # prepare agreement accepted email templatestr = (db.session.query(Contract).filter( Contract.contractTypeId == ContractType.id).filter( ContractType.contractType == 'race services' ).filter( Contract.templateTypeId == TemplateType.id).filter( TemplateType.templateType == 'agreement accepted view').one()).block template = Template(templatestr) subject = '{}ACCEPTED - FSRC Race Support Agreement: {} - {}'.format( annotation, eventdb.race.race, eventdb.date) elif eventdb.state.state == STATE_CONTRACT_SENT: # send contract mail to client templatestr = (db.session.query(Contract).filter( Contract.contractTypeId == ContractType.id).filter( ContractType.contractType == 'race services' ).filter( Contract.templateTypeId == TemplateType.id).filter( TemplateType.templateType == 'contract email').one()).block template = Template(templatestr) subject = '{}FSRC Race Support Agreement: {} - {}'.format( annotation, eventdb.race.race, eventdb.date) # state must be STATE_COMMITTED or STATE_CONTRACT_SENT, else logic error else: raise parameterError( 'editor_method_posthook(): bad state seen for {}: {}'. format(form['addlaction'], eventdb.state.state)) # merge database fields into template and send email mergefields = deepcopy(eventdb.__dict__) mergefields[ 'viewcontracturl'] = 'https://docs.google.com/document/d/{}/view'.format( docid) mergefields[ 'downloadcontracturl'] = 'https://docs.google.com/document/d/{}/export?format=pdf'.format( docid) # need to bring in full path for email, so use url_root mergefields[ 'acceptcontracturl'] = request.url_root[:-1] + url_for( 'frontend.acceptagreement', docid=docid) mergefields['servicenames'] = [ s.service for s in eventdb.services ] mergefields['event'] = eventdb.race.race html = template.render(mergefields) tolist = eventdb.client.contactEmail cclist = current_app.config['CONTRACTS_CC'] fromlist = current_app.config['CONTRACTS_CONTACT'] sendmail(subject, fromlist, tolist, html, ccaddr=cclist)
def post(self): try: requestdata = get_request_data(request.form) # verify user can write the data, otherwise abort (adapted from loutilities.tables._editormethod) # there should be one 'id' in this form data, 'keyless' if not self.permission(requestdata['keyless']['position_id']): db.session.rollback() cause = 'operation not permitted for user' return jsonify(error=cause) # get current members who previously held position on effective date effectivedate = requestdata['keyless']['effective'] effectivedatedt = dtrender.asc2dt(effectivedate).date() currmembers = members_active(self.position, effectivedate) # there may be a qualifier for this position, e.g., "interim"; # normally blank so set to None if so for backwards compatibility with the database after # initial conversion to include qualifier qualifier = requestdata['keyless']['qualifier'] if not qualifier: qualifier = None # get the members which admin wants to be in the position on the effective date # separator must match afterdatatables.js else if (location.pathname.includes('/positions')) # (if empty string is returned, there were no memberids, so use empty list) resultmemberids = [] if requestdata['keyless']['members']: resultmemberids = requestdata['keyless']['members'].split(', ') resultmembers = [ LocalUser.query.filter_by(id=id).one() for id in resultmemberids ] # terminate all future user/positions in this position as we're basing our update on effectivedate assertion by admin # delete all of these which are strictly in the future currfuturemembers = members_active_currfuture( self.position, onorafter=effectivedate) for member in currfuturemembers: ups = member_positions(member, self.position, onorafter=effectivedate) for up in ups: if up.startdate > effectivedatedt: current_app.logger.debug( f'organization_admin.PositionWizardApi.post: deleting {up.user.name} {up.position.position} {up.startdate}' ) db.session.delete(up) db.session.flush() # get current members who previously held position on effective date currmembers = members_active(self.position, effectivedate) # terminate all current members in this position who should not remain in the result set # use date one day before effective date for new finish date previousdatedt = dtrender.asc2dt(effectivedate).date() - timedelta( 1) for currmember in currmembers: ups = member_position_active(currmember, self.position, effectivedate) # more than one returned implies data error, needs to be fixed externally if len(ups) > 1: db.session.rollback() cause = 'existing position "{}" date overlap detected for {} on {}. Use ' \ '<a href="{}" target=_blank>Position Dates view</a> ' \ 'to fix before proceeding'.format(self.position.position, currmember.name, effectivedate, page_url_for('admin.positiondates', interest=g.interest)) return jsonify(error=cause) # also if none were returned there is some logic error, should not happen because currmembers pulled # in current records if not ups: db.session.rollback() cause = f'logic error: {currmember.name} not found for {self.position.position} on {effectivedate}. Please report to administrator' return jsonify(error=cause) currup = ups[0] # if the current member isn't one of the members in the position starting effective date, if currmember not in resultmembers: # overwrite finishdate -- maybe this was empty or maybe it had a date in it, either way now finished # day before effective date currup.finishdate = previousdatedt # if the finish date is now before the start date, we can delete this record, to be tidy if currup.finishdate < currup.startdate: db.session.delete(currup) db.session.flush() # loop through all members who are to be in the position as of effective date for resultmember in resultmembers: # check user/positions for this member on or after effective date ups = member_positions(resultmember, self.position, onorafter=effectivedate) # create new record for all resultmembers not already in the position # if the new member has a future record, move date the start of the future record to the effective date if resultmember not in currmembers: # normal case is no future records, so create a new record as of effectivedate if len(ups) == 0: thisups = UserPosition( interest=localinterest(), user=resultmember, position=self.position, startdate=effectivedatedt, qualifier=qualifier, ) db.session.add(thisups) # if resultmember is in currmembers, but the qualifier has changed # NOTE: ups[0] is the user/position which is active on the effective date elif ups and ups[0].qualifier != qualifier: currup = ups[0] if len(ups) > 1: # logic prevents use of futureup if not defined futureup = ups[1] # overwrite finishdate -- maybe this was empty or maybe it had a date in it, either way now finished # day before effective date finishdate = currup.finishdate currup.finishdate = previousdatedt # if the finish date is now before the start date, we can delete this record, to be tidy if currup.finishdate < currup.startdate: db.session.delete(currup) db.session.flush() # normal case there's only one current or future user/position, so create new one to follow this one if len(ups) == 1: thisups = UserPosition( interest=localinterest(), user=resultmember, position=self.position, startdate=effectivedatedt, finishdate=None, qualifier=qualifier, ) db.session.add(thisups) # if there's a future user/position, and it was right after the current, the qualifier has most likely changed # assuming the qualifer of the future user/position is the new qualifier, move the start date of the future record # NOTE: there should be no future records at this point, so this clause should not get executed elif futureup.qualifier == qualifier and futureup.startdate == finishdate + timedelta( 1): futureup.startdate = previousdatedt + timedelta(1) # commit all the changes and return success # NOTE: in afterdatatables.js else if (location.pathname.includes('/positions')) # table is redrawn on submitComplete in case this action caused visible changes output_result = {'status': 'success'} db.session.commit() return jsonify(output_result) except Exception as e: exc = ''.join(format_exception_only(type(e), e)) output_result = { 'status': 'fail', 'error': 'exception occurred:\n{}'.format(exc) } # roll back database updates and close transaction db.session.rollback() current_app.logger.error(format_exc()) return jsonify(output_result)
def editor_method_posthook(self, form): #---------------------------------------------------------------------- ''' send contract to client contact if asked to do so, after processing put() note row has already been committed to the database, so can be retrieved ''' # someday we might allow multiple records to be processed in a single request # pull record(s) from database and save as flat dotted record data = get_request_data(form) for thisid in data: sponsordb = Sponsor.query.filter_by(id=thisid).one_or_none() # if we're creating, we just flushed the row, but the id in the form was 0 # retrieve the created row through saved id if not sponsordb: thisid = self.created_id sponsordb = Sponsor.query.filter_by(id=thisid).one() # the following can be true only for put() [edit] method if 'addlaction' in form and form['addlaction'] in [ 'sendcontract', 'resendcontract' ]: folderid = current_app.config['CONTRACTS_DB_FOLDER'] # need an instance of contract manager to take care of saving the contract cm = ContractManager( contractType='race sponsorship', templateType='sponsor agreement', driveFolderId=folderid, doctype='html', ) racedate = SponsorRaceDate.query.filter_by( race_id=sponsordb.race.id, raceyear=sponsordb.raceyear).one() # bring in subrecords garbage = sponsordb.race garbage = sponsordb.client garbage = sponsordb.level # calculate the benefits (see https://stackoverflow.com/questions/40699642/how-to-query-many-to-many-sqlalchemy) benefitsdb = SponsorBenefit.query.join( SponsorBenefit.levels).filter( SponsorLevel.id == sponsordb.level.id).order_by( SponsorBenefit.order).all() benefits = [b.benefit for b in benefitsdb] # calculate display for coupon count. word (num) if less than 10, otherwise num # but note there may not be a coupon count # ccouponcount is capitalized ncoupons = sponsordb.level.couponcount if ncoupons: if ncoupons < 10: wcoupons = 'zero one two three four five six seven eight nine'.split( )[ncoupons] couponcount = '{} ({})'.format( wcoupons, ncoupons) if ncoupons else None else: couponcount = str(ncoupons) ccouponcount = couponcount.capitalize() else: couponcount = None ccouponcount = None # pick up variables variablesdb = SponsorRaceVbl.query.filter_by( race_id=sponsordb.race.id).all() variables = {v.variable: v.value for v in variablesdb} # if we are generating a new version of the contract if form['addlaction'] == 'sendcontract': # set up dateagreed, if not already there if not sponsordb.dateagreed: sponsordb.dateagreed = dt.dt2asc(date.today()) # additional fields for contract addlfields = { '_date_': humandt.dt2asc(dt.asc2dt(sponsordb.dateagreed)), '_racedate_': humandt.dt2asc(dt.asc2dt(racedate.racedate)), '_rdcertlogo_': pathjoin(current_app.static_folder, 'rd-cert-logo.png'), '_raceheader_': '<img src="{}" width=6in>'.format( pathjoin( current_app.static_folder, '{}-header.png'.format( sponsordb.race.raceshort.lower()))), '_benefits_': benefits, '_raceloc_': racedate.raceloc, '_racebeneficiary_': racedate.beneficiary, '_couponcount_': ccouponcount, # ok to assume this is first word in sentence } addlfields.update(variables) # generate contract if debug: current_app.logger.debug( 'editor_method_posthook(): (before create()) sponsordb.__dict__={}' .format(sponsordb.__dict__)) docid = cm.create( '{} {} {} Sponsor Agreement'.format( sponsordb.raceyear, sponsordb.race.raceshort, sponsordb.client.client), sponsordb, addlfields=addlfields, ) # update database to show contract sent/agreed sponsordb.state = State.query.filter_by( state=STATE_COMMITTED).one() sponsordb.contractDocId = docid # find index with correct id and show database updates for resprow in self._responsedata: if resprow['rowid'] == thisid: resprow['state'] = { key: val for (key, val) in list( sponsordb.state.__dict__.items()) if key[0] != '_' } resprow['dateagreed'] = sponsordb.dateagreed resprow['contractDocId'] = sponsordb.contractDocId # configure coupon provider with coupon code (supported providers) if sponsordb.race.couponprovider and sponsordb.level.couponcount and sponsordb.level.couponcount > 0: expiration = racedate.racedate numregistrations = sponsordb.level.couponcount clientname = sponsordb.client.client raceid = sponsordb.race.couponproviderid couponcode = sponsordb.couponcode start = sponsordb.dateagreed if sponsordb.race.couponprovider.lower( ) == 'runsignup': with RunSignUp( key=current_app.config['RSU_KEY'], secret=current_app.config['RSU_SECRET'], debug=debug) as rsu: coupons = rsu.getcoupons(raceid, couponcode) # rsu search includes any coupons with the couponcode with the coupon string, so we need to filter coupons = [ c for c in coupons if c['coupon_code'] == couponcode ] if coupons: coupon = coupons[ -1] # should be only one entry, but last is the current one (?) coupon_id = coupon['coupon_id'] # override start with the date portion of start_date start = coupon['start_date'].split(' ')[0] else: coupon_id = None rsu.setcoupon(raceid, couponcode, start, expiration, numregistrations, clientname, coupon_id=coupon_id) # if we are just resending current version of the contract else: docid = sponsordb.contractDocId # prepare agreement email (new contract or resending) templatestr = (db.session.query(Contract).filter( Contract.contractTypeId == ContractType.id).filter( ContractType.contractType == 'race sponsorship').filter( Contract.templateTypeId == TemplateType.id).filter( TemplateType.templateType == 'sponsor email').one()).block template = Template(templatestr) subject = '{} Sponsorship Agreement for {}'.format( sponsordb.race.race, sponsordb.client.client) # bring in subrecords garbage = sponsordb.race # merge database fields into template and send email mergefields = deepcopy(sponsordb.__dict__) mergefields[ 'viewcontracturl'] = 'https://docs.google.com/document/d/{}/view'.format( docid) mergefields[ 'downloadcontracturl'] = 'https://docs.google.com/document/d/{}/export?format=pdf'.format( docid) # need to bring in full path for email, so use url_root # mergefields['_race_'] = sponsordb.race.race racedate = SponsorRaceDate.query.filter_by( race_id=sponsordb.race.id, raceyear=sponsordb.raceyear).one() mergefields['_racedate_'] = humandt.dt2asc( dt.asc2dt(racedate.racedate)) mergefields['_coupondate_'] = variables['_coupondate_'] mergefields['_couponcount_'] = couponcount html = template.render(mergefields) tolist = sponsordb.client.contactEmail rdemail = '{} <{}>'.format(sponsordb.race.racedirector, sponsordb.race.rdemail) cclist = current_app.config['SPONSORSHIPAGREEMENT_CC'] + [ rdemail ] fromlist = '{} <{}>'.format( sponsordb.race.race, current_app.config['SPONSORSHIPQUERY_CONTACT']) sendmail(subject, fromlist, tolist, html, ccaddr=cclist) # calculate and update trend calculateTrend(sponsordb) # kludge to force response data to have correct trend # TODO: remove when #245 fixed thisndx = [i['rowid'] for i in self._responsedata].index(thisid) self._responsedata[thisndx]['trend'] = sponsordb.trend