/* ** Copyright (c) 2002 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public ** License as published by the Free Software Foundation; either ** version 2 of the License, or (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ** General Public License for more details. ** ** You should have received a copy of the GNU General Public ** License along with this library; if not, write to the ** Free Software Foundation, Inc., 59 Temple Place - Suite 330, ** Boston, MA 02111-1307, USA. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code used to generate web pages for ** processing trouble and enhancement tickets. */ #include "config.h" #include "ticket.h" #include /* ** If the "notify" configuration parameter exists in the CONFIG ** table and is not an empty string, then make various % substitutions ** on that string and execute the result. */ void ticket_notify(int tn, int first_change, int last_change, int atn){ const char *zNotify; char *zCmd; int i, j, c; int cmdSize; int cnt[128]; const char *azSubst[128]; static const struct { int key; char *zColumn; } aKeys[] = { { 'a', "assignedto" }, /* A - e-mail address of assignedto person */ { 'c', "contact" }, { 'd', "description" }, /* D - description, HTML formatted */ /* f - First TKTCHNG rowid of change set; zero if new record */ /* h = attacHment number if change is a new attachment; zero otherwise */ /* l - Last TKTCHNG rowid of change set; zero if new record */ /* n - ticket number */ /* p - project name */ { 'r', "remarks" }, /* R - remarks, HTML formatted */ { 's', "status" }, { 't', "title" }, /* u - current user */ { 'w', "owner" }, { 'y', "type" }, { '1', "extra1" }, { '2', "extra2" }, { '3', "extra3" }, { '4', "extra4" }, { '5', "extra5" }, }; zNotify = db_config("notify",0); if( zNotify==0 || zNotify[0]==0 ) return; memset(cnt, 0, sizeof(cnt)); memset(azSubst, 0, sizeof(azSubst)); for(i=0; zNotify[i]; i++){ if( zNotify[i]=='%' ){ c = zNotify[i+1] & 0x7f; cnt[c&0x7f]++; } } if( cnt['n']>0 ){ azSubst['n'] = mprintf("%d", tn); } if( cnt['f']>0 ){ azSubst['f'] = mprintf("%d", first_change); } if( cnt['h']>0 ){ azSubst['h'] = mprintf("%d", atn); } if( cnt['l']>0 ){ azSubst['l'] = mprintf("%d", last_change); } if( cnt['u']>0 ){ azSubst['u'] = mprintf("%s", g.zUser); } if( cnt['p']>0 ){ azSubst['p'] = mprintf("%s", g.zName); } if( cnt['D']>0 ){ /* ensure we grab a description */ cnt['d']++; } if( cnt['R']>0 ){ /* ensure we grab remarks */ cnt['r']++; } if( cnt['A']>0 ){ azSubst['A'] = db_short_query("SELECT user.email FROM ticket, user " "WHERE ticket.tn=%d and ticket.assignedto=user.id", tn); } for(i=0; i0 ){ azSubst[c] = db_short_query("SELECT %s FROM ticket WHERE tn=%d", aKeys[i].zColumn, tn); } } if( cnt['c']>0 && azSubst['c'][0]==0 ){ azSubst['c'] = db_short_query("SELECT user.email FROM ticket, user " "WHERE ticket.tn=%d and ticket.owner=user.id", tn); } if( cnt['D'] ){ azSubst['D'] = format_formatted( azSubst['d'] ); cnt['d']--; } if( cnt['R'] ){ azSubst['R'] = format_formatted( azSubst['r'] ); cnt['r']--; } /* Sanitize the strings to be substituted by removing any single-quotes ** and backslashes. ** ** That way, the notify command can contains strings like '%d' or '%r' ** (surrounded by quotes) and a hostile user cannot insert arbitrary ** shell commands. Also figure out how much space is needed to hold ** the string after substitutes occur. */ cmdSize = strlen(zNotify)+1; for(i=0; i70 ){ zErrMsg = "Please make the title no more than 70 characters long."; } if( zErrMsg==0 && zTitle[0] && zType[0] && zDesc[0] && P("submit") && (zContact[0] || !g.isAnon) ){ int tn; time_t now; const char *zState; db_execute("BEGIN"); az = db_query("SELECT max(tn)+1 FROM ticket"); tn = atoi(az[0]); if( tn<=0 ) tn = 1; time(&now); zState = db_config("initial_state", "new"); db_execute( "INSERT INTO ticket(tn, type, status, origtime, changetime, " " version, assignedto, severity, priority, derivedfrom, " " subsystem, owner, title, description, contact) " "VALUES(%d,'%q','%q',%d,%d,'%q','%q',%d,%d,'%q','%q','%q','%q','%q','%q')", tn, zType, zState, now, now, zVers, zWho, severity, priority, zFrom, zSubsys, g.zUser, zTitle, zDesc, zContact ); for(i=1; i<=5; i++){ const char *zVal; char zX[3]; bprintf(zX,sizeof(zX),"x%d",i); zVal = P(zX); if( zVal && zVal[0] ){ db_execute("UPDATE ticket SET extra%d='%q' WHERE tn=%d", i, zVal, tn); } } db_execute("COMMIT"); ticket_notify(tn, 0, 0, 0); cgi_redirect(mprintf("tktview?tn=%d",tn)); return; }else if( P("submit") ){ if( zTitle[0]==0 ){ zErrMsg = "Please enter a title."; }else if( zDesc[0]==0 ){ zErrMsg = "Please enter a description."; }else if( zContact[0]==0 && g.isAnon ){ zErrMsg = "Please enter your contact information."; } } common_standard_menu("tktnew", 0); common_add_help_item("CvstracTicket"); common_add_action_item( "index", "Cancel"); common_header("Create A New Ticket"); if( zErrMsg ){ @
@ %h(zErrMsg) @
} @
@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ if( g.okWrite ){ @ @ @ @ @ az = db_query("SELECT '', '' UNION ALL " "SELECT name, value FROM enums WHERE type='subsys'"); if( az[0] && az[1] && az[2] ){ @ @ @ @ } } @ @ @ @ if( g.isAnon ){ @ @ @ @ @ } for(i=1; i<=5; i++){ char **az; const char *zDesc; const char *zName; char zX[3]; char zExName[100]; bprintf(zExName,sizeof(zExName),"extra%d_desc",i); zDesc = db_config(zExName, 0); if( zDesc==0 ) continue; bprintf(zExName,sizeof(zExName),"extra%d_name",i); zName = db_config(zExName, 0); if( zName==0 ) continue; az = db_query("SELECT name, value FROM enums " "WHERE type='extra%d'", i); bprintf(zX, sizeof(zX), "x%d", i); @ @ @ @ @ } @ @ @ @ @ @ @ @ @ @ @ @
@ Enter a one-line summary of the problem:
@ @
Type: cgi_v_optionmenu2(0, "y", zType, (const char**)db_query( "SELECT name, value FROM enums WHERE type='type'")); @ What type of ticket is this?
@ Version: @ @ Enter the version and/or build number of the product @ that exhibits the problem. @
@ Severity: cgi_optionmenu(0, "r", zSev, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); @ @ How debilitating is the problem? "1" is a show-stopper defect with @ no workaround. "2" is a major defect with a workaround. "3" @ is a mid-level defect. "4" is an annoyance. "5" is a cosmetic @ defect or a nice-to-have feature request. @
@ Priority: cgi_optionmenu(0, "p", zPri, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); @ @ How quickly do you need this ticket to be resolved? @ "1" means immediately. @ "2" means before the next build. @ "3" means before the next release. @ "4" means implement as time permits. @ "5" means defer indefinitely. @
@ Assigned To: az = db_query("SELECT id FROM user UNION SELECT '' ORDER BY id"); cgi_v_optionmenu(0, "w", zWho, (const char **)az); db_query_free(az); @ @ To what user should this problem be assigned? @
@ Subsystem: cgi_v_optionmenu2(4, "s", zSubsys, (const char**)az); db_query_free(az); @ @ Which component is showing a problem? @
@ Derived From: @ @ Is this related to an existing ticket? @
@ Contact: @ @ Enter a phone number or e-mail address where a developer can @ contact you with questions about this ticket. The information @ you enter will be available to the developers only and will not @ be visible to general users. @
@ %h(zName): if( az==0 || az[0]==0 ){ @ }else{ cgi_v_optionmenu2(0, zX, PD(zX,az[0]), (const char**)az); } @ /* description is already HTML markup */ @ %s(zDesc) @
@ Enter a detailed description of the problem. For code defects, @ be sure to provide details on exactly how the problem can be @ reproduced. Provide as much detail as possible. @ Formatting hints. @
@ if( isPreview ){ @
Description Preview: @
output_formatted(zDesc, 0); @
} if( g.okWrite ){ @
Note: If you want to include a large script or binary file @ with this ticket you will be given an opportunity to add attachments @ to the ticket after the ticket has been created. Do not paste @ large scripts or screen dumps in the description. } @
@ @ @ Preview the formatting of the description. @
@ @ @ After filling in the information about, press this button to create @ the new ticket. @
@
@ @
@

Formatting Hints:

append_formatting_hints(); common_footer(); } /* ** Return TRUE if it is ok to undo a ticket change that occurred at ** chngTime and was made by user zUser. ** ** A ticket change can be undone by: ** ** * The Setup user at any time. ** ** * By the registered user who made the change within 24 hours of ** the change. ** ** * By the Delete user within 24 hours of the change if the change ** was made by anonymous. */ static int ok_to_undo_change(int chngTime, const char *zUser){ if( g.okSetup ){ return 1; } if( g.isAnon || chngTimeIf you really want to remove the last edit to ticket #%d(tn) @ then click on the "OK" link below. Otherwise, click on "Cancel".

@
@ @ @ @ @ @
@ @ @ @
@
common_footer(); return; } /* Make sure the change we are requested to undo is the vary last ** change. */ z = db_short_query("SELECT max(chngtime) FROM tktchng WHERE tn=%d", tn); if( z==0 || tm!=atoi(z) ){ goto undo_finished; } /* If we get this far, it means the user has confirmed that they ** want to undo the last change to the ticket. */ db_execute("BEGIN"); az = db_query("SELECT fieldid, oldval FROM tktchng " "WHERE tn=%d AND user='%q' AND chngtime=%d", tn, zUser, tm); for(i=0; az[i]; i+=2){ db_execute("UPDATE ticket SET %s='%q' WHERE tn=%d", az[i], az[i+1], tn); } db_execute("DELETE FROM tktchng WHERE tn=%d AND user='%q' AND chngtime=%d", tn, zUser, tm); db_execute("COMMIT"); undo_finished: cgi_redirect(mprintf("tkthistory?tn=%d",tn)); } /* ** Extract the ticket number and report number from the "tn" query ** parameter. */ #if 0 /* NOT USED */ static void extract_codes(int *pTn, int *pRn){ *pTn = *pRn = 0; sscanf(PD("tn",""), "%d,%d", pTn, pRn); } #endif static void output_tkt_chng(char **azChng){ time_t thisDate; struct tm *pTm; char zDate[100]; char zPrefix[200]; char zSuffix[100]; char *z; const char *zType = (atoi(azChng[5])==0) ? "Check-in" : "Milestone"; thisDate = atoi(azChng[0]); pTm = localtime(&thisDate); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm); if( azChng[2][0] ){ bprintf(zPrefix, sizeof(zPrefix), "%h [%.20h] on branch %.50h: ", zType, azChng[1], azChng[2]); }else{ bprintf(zPrefix, sizeof(zPrefix), "%h [%.20h]: ", zType, azChng[1]); } bprintf(zSuffix, sizeof(zSuffix), " (By %.30h)", azChng[3]); @ %h(zDate) @ common_icon("dot"); @ @ output_formatted(zPrefix, 0); z = azChng[4]; if( output_trim_message(z, MN_CKIN_MSG, MX_CKIN_MSG) ){ output_formatted(z, 0); @  [...] }else{ output_formatted(z, 0); } output_formatted(zSuffix, 0); @ } /* ** WEBPAGE: /tktview ** ** A webpage for viewing the details of a ticket */ void ticket_view(void){ int i, j, nChng; int tn = 0, rn = 0; char **az; char **azChng; char **azDrv; char *z; const char *azExtra[5]; char zPage[30]; const char *zTn; login_check_credentials(); if( !g.okRead ){ login_needed(); return; } throttle(1,0); history_update(0); zTn = PD("tn",""); sscanf(zTn, "%d,%d", &tn, &rn); if( tn<=0 ){ cgi_redirect("index"); return; } bprintf(zPage,sizeof(zPage),"%d",tn); common_standard_menu("tktview", "search?t=1"); if( rn>0 ){ common_replace_nav_item(mprintf("rptview?rn=%d", rn), "Report"); common_add_action_item(mprintf("tkthistory?tn=%d,%d", tn, rn), "History"); }else{ common_add_action_item(mprintf("tkthistory?tn=%d", tn), "History"); } if( g.okWrite ){ if( rn>0 ){ common_add_action_item(mprintf("tktedit?tn=%d,%d",tn,rn), "Edit"); }else{ common_add_action_item(mprintf("tktedit?tn=%d",tn), "Edit"); } if( attachment_max()>0 ){ common_add_action_item(mprintf("attach_add?tn=%d",tn), "Attach"); } } add_tkt_tools(0,tn); common_add_help_item("CvstracTicket"); /* Check to see how many "extra" ticket fields are defined */ azExtra[0] = db_config("extra1_name",0); azExtra[1] = db_config("extra2_name",0); azExtra[2] = db_config("extra3_name",0); azExtra[3] = db_config("extra4_name",0); azExtra[4] = db_config("extra5_name",0); /* Get the record out of the database. */ db_add_functions(); az = db_query("SELECT " " type," /* 0 */ " status," /* 1 */ " ldate(origtime)," /* 2 */ " ldate(changetime)," /* 3 */ " derivedfrom," /* 4 */ " version," /* 5 */ " assignedto," /* 6 */ " severity," /* 7 */ " priority," /* 8 */ " subsystem," /* 9 */ " owner," /* 10 */ " title," /* 11 */ " description," /* 12 */ " remarks, " /* 13 */ " contact," /* 14 */ " extra1," /* 15 */ " extra2," /* 16 */ " extra3," /* 17 */ " extra4," /* 18 */ " extra5 " /* 19 */ "FROM ticket WHERE tn=%d", tn); if( az[0]==0 ){ cgi_redirect("index"); return; } azChng = db_query( "SELECT chng.date, chng.cn, chng.branch, chng.user, chng.message, chng.milestone " "FROM xref, chng WHERE xref.tn=%d AND xref.cn=chng.cn " "ORDER BY chng.milestone ASC, chng.date DESC", tn); azDrv = db_query( "SELECT tn,title FROM ticket WHERE derivedfrom=%d", tn); common_header("Ticket #%d", tn); @

Ticket %d(tn): %h(az[11])

@
output_formatted(az[12], zPage); @
@ @ @
@ @ @
if( az[13][0]==0 ){ @ [Add remarks] } else { @ [Append remarks] } @
@

Remarks:

@
output_formatted(az[13], zPage); @
if( az[13][0]!=0 ){ @ @
@ @ @
@ [Append remarks] @
@ } @ @

Properties:

@ @
@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ if( g.okWrite && !g.isAnon ){ @ @ if( strchr(az[14],'@') ){ @ }else{ @ } @ j = 0; } else { j = 1; } for(i=0; i<5; i++){ if( azExtra[i]==0 ) continue; if( j==0 ){ @ }else{ @ } @ @ if( j==0 ){ j = 1; }else{ @ j = 0; } } if( j==1 ){ @ } @ @ @
Type:%h(az[0])         Version:%h(az[5]) 
Status:%h(az[1])        Created:%h(az[2])
Severity:%h(az[7])         Last Change:%h(az[3])
Priority:%h(az[8])         Subsystem:%h(az[9]) 
Assigned To:%h(az[6])         Derived From: z = extract_integer(az[4]); if( z && z[0] ){ z = mprintf("#%s",z); output_formatted(z,zPage); }else{ @   } @
Creator:%h(az[10])         Contact: @ %h(az[14]) %h(az[14]) 
%h(azExtra[i]):%h(az[15+i]) 
@
if( azDrv[0] ){ int i; @

Derived Tickets:

@ for(i=0; azDrv[i]; i+=2){ @ @ @ } @
z = mprintf("#%s",azDrv[i]); output_formatted(z,zPage); @ common_icon("ptr1"); @ output_formatted(azDrv[i+1],0); @
} nChng = 0; if( azChng[0] && azChng[5] && atoi(azChng[5])==0 ){ int i; @

Related Check-ins:

@ for(i=0; azChng[i]; i+=6){ /* Milestones are handeld in loop below */ if( atoi(azChng[i+5]) ) break; nChng++; output_tkt_chng(&azChng[i]); } @
} if( azChng[0] && azChng[nChng*6] ){ int i; @

Related Milestones:

@ for(i=nChng*6; azChng[i]; i+=6){ output_tkt_chng(&azChng[i]); } @
} attachment_html(zPage,"

Attachments:

\n
","
"); common_footer(); } /* ** Check to see if the current user is authorized to delete ticket tn. ** Return true if they are and false if not. ** ** Ticket deletion rules: ** ** * The setup user can delete any ticket at any time. ** ** * Users other than anonymous with Delete privilege can delete ** a ticket that was originated by anonymous and has no change ** by anyone other than anonymous and is less than 24 hours old. ** ** * Anonymous users can never delete tickets even if they have ** Delete privilege */ static int ok_to_delete_ticket(int tn){ time_t cutoff = time(0)-86400; if( g.okSetup ){ return 1; } if( g.isAnon || !g.okDelete ){ return 0; } if( db_exists( "SELECT 1 FROM ticket" " WHERE tn=%d AND (owner!='anonymous' OR origtime<%d)" "UNION ALL " "SELECT 1 FROM tktchng" " WHERE tn=%d AND (user!='anonymous' OR chngtime<%d)", tn, cutoff, tn, cutoff) ){ return 0; } return 1; } /* ** WEBPAGE: /tktedit ** ** A webpage for making changes to a ticket */ void ticket_edit(void){ static struct { char *zColumn; /* Name of column in the database */ char *zName; /* Name of corresponding query parameter */ int preserveSpace; /* Preserve initial spaces in text */ int numeric; /* Field is a numeric value */ const char *zOld; /* Current value of this field */ const char *zNew; /* Value of the query parameter */ } aParm[] = { { "type", "y", 0, 0, }, /* 0 */ { "status", "s", 0, 0, }, /* 1 */ { "derivedfrom", "d", 0, 1, }, /* 2 */ { "version", "v", 0, 0, }, /* 3 */ { "assignedto", "a", 0, 0, }, /* 4 */ { "severity", "e", 0, 1, }, /* 5 */ { "priority", "p", 0, 0, }, /* 6 */ { "subsystem", "m", 0, 0, }, /* 7 */ { "owner", "w", 0, 0, }, /* 8 */ { "title", "t", 0, 0, }, /* 9 */ { "description", "c", 1, 0, }, /* 10 */ { "remarks", "r", 1, 0, }, /* 11 */ { "contact", "n", 0, 0, }, /* 12 */ { "extra1", "x1", 0, 0, }, /* 13 */ { "extra2", "x2", 0, 0, }, /* 14 */ { "extra3", "x3", 0, 0, }, /* 15 */ { "extra4", "x4", 0, 0, }, /* 16 */ { "extra5", "x5", 0, 0, }, /* 17 */ }; int tn = 0; int rn = 0; int nField; int i, j; int cnt; int isPreview; const char *zChngList; char *zSep; char **az; const char **azUsers; int *aChng, *aMs; int nChng, nMs; int nExtra; const char *azExtra[5]; char zPage[30]; char zSQL[2000]; char *zErrMsg = 0; login_check_credentials(); if( !g.okWrite ){ login_needed(); return; } throttle(1,1); isPreview = P("pre")!=0; sscanf(PD("tn",""), "%d,%d", &tn, &rn); if( tn<=0 ){ cgi_redirect("index"); return; } bprintf(zPage,sizeof(zPage),"%d",tn); history_update(0); if( P("del1") && ok_to_delete_ticket(tn) ){ char *zTitle = db_short_query("SELECT title FROM ticket " "WHERE tn=%d", tn); if( zTitle==0 ) cgi_redirect("index"); common_add_action_item(mprintf("tktedit?tn=%h",PD("tn","")), "Cancel"); common_header("Are You Sure?"); @
@

You are about to delete all traces of ticket output_ticket(tn); @  %h(zTitle) from @ the database. This is an irreversible operation. All records @ related to this ticket will be removed and cannot be recovered.

@ @ @ @ @
common_footer(); return; } if( P("del2") && ok_to_delete_ticket(tn) ){ db_execute( "BEGIN;" "DELETE FROM ticket WHERE tn=%d;" "DELETE FROM tktchng WHERE tn=%d;" "DELETE FROM xref WHERE tn=%d;" "DELETE FROM attachment WHERE tn=%d;" "COMMIT;", tn, tn, tn, tn); if( rn>0 ){ cgi_redirect(mprintf("rptview?rn=%d",rn)); }else{ cgi_redirect("index"); } return; } /* Check to see how many "extra" ticket fields are defined */ nField = sizeof(aParm)/sizeof(aParm[0]); azExtra[0] = db_config("extra1_name",0); azExtra[1] = db_config("extra2_name",0); azExtra[2] = db_config("extra3_name",0); azExtra[3] = db_config("extra4_name",0); azExtra[4] = db_config("extra5_name",0); for(i=nExtra=0; i<5; i++){ if( azExtra[i]!=0 ){ nExtra++; }else{ aParm[13+i].zColumn = 0; } } /* Construct a SELECT statement to extract all information we ** need from the ticket table. */ j = 0; appendf(zSQL,&j,sizeof(zSQL),"SELECT"); zSep = " "; for(i=0; i0 AND xref.tn=%d ORDER BY xref.cn", tn ); for(nMs=0; az[nMs]; nMs++){} aMs = malloc( sizeof(int)*nMs ); if( aMs==0 ) nMs = 0; for(i=0; i=nChng ){ db_execute("INSERT INTO xref(tn,cn) VALUES(%d,%d)", tn, aMs[i]); } } db_execute("COMMIT"); if( cnt ){ ticket_notify(tn, first_change, last_change, 0); } if( rn>0 ){ cgi_redirect(mprintf("rptview?rn=%d",rn)); }else{ cgi_redirect(mprintf("tktview?tn=%d,%d",tn,rn)); } return; } /* Print the header. */ common_add_action_item( mprintf("tktview?tn=%d,%d", tn, rn), "Cancel"); if( ok_to_delete_ticket(tn) ){ common_add_action_item( mprintf("tktedit?tn=%d,%d&del1=1", tn, rn), "Delete"); } common_add_help_item("CvstracTicket"); common_header("Edit Ticket #%d", tn); @
@ @ @ Ticket Number: %d(tn)
if( zErrMsg ){ @
@ %h(zErrMsg) @
} @ @ Title: @
@ @ Description: @ (See
formatting hints)
@
if( isPreview ){ @
output_formatted(aParm[10].zNew, zPage); @  

} @ @ Remarks: @ (See formatting hints)
@
if( isPreview ){ @
output_formatted(aParm[11].zNew, zPage); @  

} @ @ @ Status: cgi_v_optionmenu2(0, "s", aParm[1].zNew, (const char**)db_query( "SELECT name, value FROM enums WHERE type='status'")); @ @     @ @ @ Type: cgi_v_optionmenu2(0, "y", aParm[0].zNew, (const char**)db_query( "SELECT name, value FROM enums WHERE type='type'")); @ @     @ @ @ @ Severity: cgi_optionmenu(0, "e", aParm[5].zNew, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); @ @     @ @ @ Assigned To: azUsers = (const char**)db_query( "SELECT id FROM user UNION SELECT '' ORDER BY id"); cgi_v_optionmenu(0, "a", aParm[4].zNew, azUsers); @ @     @ @ @ Subsystem: cgi_v_optionmenu2(0, "m", aParm[7].zNew, (const char**)db_query( "SELECT '','' UNION ALL " "SELECT name, value FROM enums WHERE type='subsys'")); @ @     @ @ @ Version: @ @     @ @ @ Derived From: @ @     @ @ @ Priority: cgi_optionmenu(0, "p", aParm[6].zNew, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); @ @     @ @ @ Owner: cgi_v_optionmenu(0, "w", aParm[8].zNew, azUsers); @ @     @ if( !g.isAnon ){ @ @ Contact: @ @     @ } for(i=0; i<5; i++){ char **az; char zX[3]; if( azExtra[i]==0 ) continue; az = db_query("SELECT name, value FROM enums " "WHERE type='extra%d'", i+1); bprintf(zX, sizeof(zX), "x%d", i+1); @ @ %h(azExtra[i]): if( az && az[0] ){ cgi_v_optionmenu2(0, zX, aParm[13+i].zNew, (const char **)az); }else{ @ } db_query_free(az); @ @     @ } @ @ Associated Check-ins: cgi_printf("\n"); @ @     @ @ Associated Milestones: cgi_printf("\n"); @ @     @ @

@ @     @ if( ok_to_delete_ticket(tn) ){ @     @ } @

@ @
attachment_html(mprintf("%d",tn),"

Attachments

", "
"); @ @ @
@

Formatting Hints:

append_formatting_hints(); common_footer(); } /* ** WEBPAGE: /tktappend ** ** Append remarks to a ticket */ void ticket_append(void){ int tn, rn; char zPage[30]; int doPreview; int doSubmit; const char *zText; const char *zTn; char *zErrMsg = 0; char *zTktTitle; login_check_credentials(); if( !g.okWrite ){ login_needed(); return; } throttle(1,1); tn = rn = 0; zTn = PD("tn",""); sscanf(zTn, "%d,%d", &tn, &rn); if( tn<=0 ){ cgi_redirect("index"); return; } bprintf(zPage,sizeof(zPage),"%d",tn); doPreview = P("pre")!=0; doSubmit = P("submit")!=0; zText = remove_blank_lines(PD("r","")); if( doSubmit ){ zErrMsg = is_edit_allowed(0,zText); if( zText[0] && 0==zErrMsg ){ time_t now; struct tm *pTm; char zDate[200]; const char *zOrig; char *zNew; char *zSpacer = " {linebreak}\n"; char *zHLine = "\n\n----\n"; char **az; int change; zOrig = db_short_query("SELECT remarks FROM ticket WHERE tn=%d", tn); zOrig = remove_blank_lines(zOrig); time(&now); pTm = localtime(&now); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm); if( isspace(zText[0]) && isspace(zText[1]) ) zSpacer = "\n\n"; if( zOrig[0]==0 ) zHLine = ""; zNew = mprintf("%s_%s by %s:_%s%s", zHLine, zDate, g.zUser, zSpacer, zText); db_execute( "BEGIN;" "UPDATE ticket SET remarks='%q%q', changetime=%d WHERE tn=%d;" "INSERT INTO tktchng(tn,user,chngtime,fieldid,oldval,newval) " "VALUES(%d,'%q',%d,'remarks','%q','%q%q');" "COMMIT;", zOrig, zNew, now, tn, tn, g.zUser, now, zOrig, zOrig, zNew ); az = db_query( "SELECT MAX(ROWID) FROM tktchng" ); change = atoi(az[0]); ticket_notify(tn, change, change, 0); cgi_redirect(mprintf("tktview?tn=%h",zTn)); } } zTktTitle = db_short_query("SELECT title FROM ticket WHERE tn=%d", tn); common_add_help_item("CvstracTicket"); common_add_action_item( mprintf("tktview?tn=%h", zTn), "Cancel"); common_header("Append Remarks To Ticket #%d", tn); if( zErrMsg ){ @
@ %h(zErrMsg) @
} @
@ @ Append to #%d(tn): cgi_href(zTktTitle, 0, 0, 0, 0, 0, "tktview?tn=%d", tn); @   @ (See formatting hints)
@ @
@

@ @     @ @

if( doPreview ){ @
output_formatted(zText, zPage); @  

} @ @
@ @
@

Formatting Hints:

append_formatting_hints(); common_footer(); } /* ** Output a ticket change record. isLast indicates it's the last ** ticket change and _might_ be subject to undo. */ static void ticket_change( time_t date, /* date/time of the change */ int tn, /* ticket number */ const char *zUser, /* user that made the change */ const char *zField, /* field that changed */ const char *zOld, /* old value */ const char *zNew, /* new value */ int isLast /* non-zero if last ticket change in the history */ ){ struct tm *pTm; char zDate[100]; char zPage[30]; bprintf(zPage,sizeof(zPage),"%d",tn); pTm = localtime(&date); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm); @
  • if( strcmp(zField,"description")==0 || strcmp(zField,"remarks")==0 ){ int len1, len2; len1 = strlen(zOld); len2 = strlen(zNew); if( len1==0 ){ @ Added %h(zField):
    output_formatted(&zNew[len1], zPage); @
    }else if( len2>len1+5 && strncmp(zOld,zNew,len1)==0 ){ @ Appended to %h(zField):
    output_formatted(&zNew[len1], zPage); @
    }else{ @ Changed %h(zField). diff_strings(1,zOld,zNew); } }else if( (!g.okWrite || g.isAnon) && strcmp(zField,"contact")==0 ){ /* Do not show contact information to unprivileged users */ @ Change %h(zField) }else if( strncmp(zField,"extra",5)==0 ){ char zLabel[30]; const char *zAlias; bprintf(zLabel,sizeof(zLabel),"%h_name", zField); zAlias = db_config(zLabel, zField); @ Change %h(zAlias) from "%h(zOld)" to "%h(zNew)" }else{ @ Change %h(zField) from "%h(zOld)" to "%h(zNew)" } @ by %h(zUser) on %h(zDate) if( isLast && ok_to_undo_change(date, zUser) ){ @ [
    Undo @ this change]

    } @
  • } /* ** Output a checkin record. */ static void ticket_checkin( time_t date, /* date/time of the change */ int cn, /* change number */ const char *zBranch, /* branch of the change, may be NULL */ const char *zUser, /* user name that made the change */ const char *zMessage /* log message for the change */ ){ struct tm *pTm; char *z; char zDate[100]; @
  • Check-in output_chng(cn); if( zBranch && zBranch[0] ){ @ on branch %h(zBranch): } else { cgi_printf(": "); /* want the : right up against the [cn] */ } z = strdup(zMessage); if( output_trim_message(z, MN_CKIN_MSG, MX_CKIN_MSG) ){ output_formatted(z, 0); @  [...] }else{ output_formatted(z, 0); } pTm = localtime(&date); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm); @ (By %h(zUser) on %h(zDate))
  • @ } /* ** Output an attachment record. */ static void ticket_attach( time_t date, /* date/time of the attachment */ int attachn, /* attachment number */ size_t size, /* size, in bytes, of the attachment */ const char *zUser, /* username that created it */ const char *zDescription, /* description of the attachment */ const char *zFilename /* name of attachment file */ ){ char zDate[100]; struct tm *pTm; pTm = localtime(&date); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm); @
  • Attachment @ %h(zFilename) @ %d(size) bytes added by %h(zUser) on %h(zDate). if( zDescription && zDescription[0] ){ @
    output_formatted(zDescription,NULL); @
    } if( ok_to_delete_attachment(date, zUser) ){ @ [delete] } @
  • } /* ** Output an inspection note. */ static void ticket_inspect( time_t date, /* date/time of the inspection */ int cn, /* change that was inspected */ const char *zInspector, /* username that did the inspection */ const char *zResult /* string describing the result */ ){ char zDate[100]; struct tm *pTm; pTm = localtime(&date); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm); @
  • Inspection report "%h(zResult)" on output_chng(cn); @  by %h(zInspector) on %h(zDate) @
  • } /* ** Output a derived ticket creation */ static void ticket_derived( time_t date, /* date/time derived ticket was created */ int tn, /* number of derived ticket */ const char* zOwner, /* creator of derived ticket */ const char *zTitle /* (currently unused) title of derived ticket */ ){ char zDate[100]; struct tm *pTm; pTm = localtime(&date); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm); @
  • Derived output_ticket(tn); @  by %h(zOwner) on %h(zDate) @
  • } /* ** WEBPAGE: /tkthistory ** ** A webpage for viewing the history of a ticket. The history is a ** chronological mix of ticket actions, checkins, attachments, etc. */ void ticket_history(void){ int tn = 0, rn = 0; int lasttn = 0; char **az; int i; char zPage[30]; const char *zTn; time_t orig; char zDate[200]; struct tm *pTm; login_check_credentials(); if( !g.okRead ){ login_needed(); return; } throttle(1,0); history_update(0); zTn = PD("tn",""); sscanf(zTn, "%d,%d", &tn, &rn); if( tn<=0 ){ cgi_redirect("index"); return; } bprintf(zPage,sizeof(zPage),"%d",tn); common_standard_menu("tktview", "search?t=1"); if( rn>0 ){ common_add_action_item(mprintf("tktview?tn=%d,%d",tn,rn), "View"); }else{ common_add_action_item(mprintf("tktview?tn=%d",tn), "View"); } common_add_help_item("CvstracTicket"); if( g.okWrite ){ if( rn>0 ){ common_add_action_item(mprintf("tktedit?tn=%d,%d",tn,rn), "Edit"); }else{ common_add_action_item(mprintf("tktedit?tn=%d",tn), "Edit"); } if( attachment_max()>0 ){ common_add_action_item(mprintf("attach_add?tn=%d",tn), "Attach"); } } add_tkt_tools(0,tn); /* Get the record from the database. */ db_add_functions(); az = db_query("SELECT title,origtime,owner FROM ticket WHERE tn=%d", tn); if( az == NULL || az[0]==0 ){ cgi_redirect("index"); return; } orig = atoi(az[1]); pTm = localtime(&orig); strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm); common_header("Ticket #%d History", tn); @

    Ticket %d(tn) History: %h(az[0])

    @
      @
    1. Created %h(zDate) by %h(az[2])
    2. /* Grab various types of ticket activities from the db. ** All must be sorted by ascending time and the first field of each ** record should be epoch time. Second field is the record type. */ az = db_query( /* Ticket changes */ "SELECT chngtime AS 'time', 1 AS 'type', " "user, fieldid, oldval, newval, NULL " "FROM tktchng WHERE tn=%d " "UNION ALL " /* Checkins */ "SELECT chng.date AS 'time', 2 AS 'type', " " chng.cn, chng.branch, chng.user, chng.message, chng.milestone " "FROM xref, chng WHERE xref.tn=%d AND xref.cn=chng.cn " "UNION ALL " /* attachments */ "SELECT date AS 'time', 3 AS 'type', atn, size, user, description, fname " "FROM attachment WHERE tn=%d " "UNION ALL " /* inspection reports */ "SELECT inspect.inspecttime AS 'time', 4 AS 'type', " "inspect.cn, inspect.inspector, inspect.result, NULL, NULL " "FROM xref, inspect " "WHERE xref.cn=inspect.cn AND xref.tn=%d " "UNION ALL " /* derived tickets. This is just the derived ticket creation. Could ** also report derived ticket changes, but we'd probably have to ** use some kind of tree representation. */ "SELECT origtime AS 'time', 5 AS 'type', tn, owner, title, NULL, NULL " "FROM ticket WHERE derivedfrom=%d " "ORDER BY 1, 2", tn, tn, tn, tn, tn); /* find the last ticket change in the list. This is necessary to allow ** someone to undo the last change. */ for(i=0; az[i]; i+=7){ int type = atoi(az[i+1]); if( type==1 ) lasttn = i; } for(i=0; az[i]; i+=7) { time_t date = atoi(az[i]); int type = atoi(az[i+1]); switch( type ){ case 1: { /* ticket change */ ticket_change(date, tn, az[i+2], az[i+3], az[i+4], az[i+5], lasttn==i); break; } case 2: { /* checkin */ ticket_checkin(date, atoi(az[i+2]), az[i+3], az[i+4], az[i+5]); break; } case 3: { /* attachment */ ticket_attach(date, atoi(az[i+2]), atoi(az[i+3]), az[i+4], az[i+5], az[i+6]); break; } case 4: { /* inspection report */ ticket_inspect(date, atoi(az[i+2]), az[i+3], az[i+4]); break; } case 5: { /* derived ticket creation */ ticket_derived(date, atoi(az[i+2]), az[i+3], az[i+4]); break; } default: /* Can't happen */ /* assert( type >= 1 && type <= 5 ); */ break; } } @
    common_footer(); }