ALM in IT Thesis Template

Document Sample
ALM in IT Thesis Template Powered By Docstoc
					eNotify: Re-Engineering a Bug Tracking Application to Meet Enterprise
                           Requirements




                               Paul J Russell




              A Thesis in the Field of Information Technology

        for the Degree of Master of Liberal Arts in Extension Studies




                            Harvard University


                                 June 2009
                                         Abstract

       This is a thesis for the degree of Master of Liberal Arts in Information

Technology (ALM in IT). The goal of this project is to remedy the scaling issues of a

widely used, server-bound, bug tracking application named eNotify.

       I designed and developed eNotify several years ago for a small group of teams. In

the intervening years it has been widely adopted by the engineering community

throughout the company. The scope of the current deployment far exceeds the original

design criteria. As a result, the application requires high maintenance costs and no longer

meets its end users’ requirements for data freshness. This project solves these issues and

positions the application for future enhancement by upgrading eNotify’s application

development framework and migrating to a distributed deployment model.

          This thesis project comprises three major development tasks and a real world

deployment test. The three development tasks cover the major steps required to:

           1. Create a scalable, easily-enhanced, web-based administration console;
           2. Convert the existing bug tracking processing engine to work with the new
              administration console;
           3. Configure the application to work in a distributed model that enables
              scalability and ease of maintenance.

       I demonstrate how a distributed design and use of a full-stack web application

development framework addresses the scaling issues of a mission critical application in a

real business environment while reducing the operating cost of running that system.
                             Author's Biographical Sketch

       Paul Russell was born April 7, 1975 in New York, New York to parents James

and Connie Russell. His family moved to Connecticut in his early teens where he

discovered music. By his late teens he was an accomplished saxophonist and tuba player.

While he preferred to play alto saxophone in jazz ensembles, Paul rarely turned down the

opportunity to play with any group and he quickly become a regular face in nearly all of

the school bands and ensembles. In his sophomore year he briefly dated a girl named

Christine whom he had met on a school bus.

       Paul’s father had a knack for both Physics and teaching. He tutored Paul, allowing

him to complete the school’s one-year Physics course in three-months, just in time for

him to take the January SAT in Physics. Paul received a near perfect score on that test

and became enamored with science for the first time. Outside school and music, Paul

trained to become an Emergency Medical Technician and volunteered with his town’s

Emergency Medical Service.

       He attended Rutgers University in New Jersey for two years where his primary

focus was studying Physics, playing Ultimate Frisbee, and volunteering at the local

Emergency Medical Service. During the summer he returned home to Connecticut where

he organized and performed in a big band concert series called Random Acts of Jazz,

which raised several thousand dollars through concert ticket sales, and donated all profits

to the elementary school music program.




                                            iv
       The experience and lessons Paul learned from running Random Acts of Jazz

provided the foundation for much of his future business success.

       Paul found it hard to find a home at Rutgers and transferred to the University of

Connecticut (UConn) where he majored in Biology. At UConn he elected to live in the

dormitory for Biology students, but on his arrival discovered that while the building was

labeled as a Biology dorm 95% of its residence were declared Computer Science majors.

His constant exposure to Computer Science seeded his interest in the field.

       Paul completed his undergraduate degree in Biology and entered the job market at

the height of the tech bubble. A friend from his Random Acts of Jazz days introduced

him to a small web collaboration software startup in Boston, Massachusetts. Lacking any

formal experience, Paul was turned down for a job twice before finally being offered and

accepting a position in Quality Assurance. He took advantage of every opportunity to

learn and quickly built up a competence with basic programming concepts. In 2000 Paul

began a Masters in Information Technology at Harvard University.

       That same year, Paul and Christine reunited. They married and now have three

children, Alex, Conner, and Allison. Paul’s strongest memories from his Master’s studies

is the sound of his family playing outside on a beautiful summer day while he labored

inside his house on Algorithms and Data Structures assignments.

        Paul is currently a Business development manager at a large technology

company. He lives in Acton, Massachusetts with his wife, three children, and three cats.

He has recently started teaching his son Alex to play the saxophone.




                                            v
                                      Dedication

        This thesis is dedicated to my grandmother, Mrs. James A. Russell and my father

James T. Russell, whose support, urging, and frequent status requests, gave me the

encouragement I needed to complete this project.




                                           vi
                                    Acknowledgments

        This work would not have been possible without the support and encouragement

of my thesis advisor Dr. John G. Norman. One of my greatest take always from this

project will be to have worked with a person with such a natural gift for teaching. His

perspective and guidance on every aspect of this project has been invaluable. Over the

course of this project I’ve come to think of him as a trusted mentor and friend.

       I would like to thank Dr. Bill Robinson, under whose supervision I chose the topic

and developed the project proposal. The selection of this project provided an opportunity

to address the requirements of my degree while providing a valuable service to my

employer.

       I am indebted to my thesis director Dr. Jeff Parker who guided me through the

final stages of the thesis project. It is a rare gift to work with someone that can be both

serious and funny at the same time. His communication style is one of the most

motivating techniques for delivering constructive criticism I’ve experienced.

       I would also like to thank my parents James and Connie Russell, and my

grandmother, Mrs. James A. Russell, who encouraged me to continue my studies after

receiving my Bachelors degree.

       I am grateful to my brother John, for joining me on this journey through a Masters

Degree, putting up with me every day at work and providing much needed comic relief

during stressful times.

       Finally, and most importantly I would like to thank my wife Christine.

Throughout my Masters studies she has often been left alone to raise our three wonderful



                                             vii
but often mischievous children while I was locked away in an office working. Her love

and support throughout the years has enabled me to complete this program. This success

is as much hers as it is mine.




                                          viii
                                               Table of Contents



Table of Contents ............................................................................................................... ix

List of Figures ................................................................................................................. xvii

Chapter 1 Introduction ........................................................................................................ 1

   Bug Tracking in a Large Company................................................................................. 2

   Legacy eNotify Application Overview ........................................................................... 4

   What eNotify Does ......................................................................................................... 6

       eNotify Server Basics ................................................................................................. 6

       Website Generator Work Flow ................................................................................... 7

       Scaling and Server Limitations ................................................................................... 8

       Legacy Application Shortcoming: eNotify Falls Victim to Its Own Success........... 10

Chapter 2 Solution Requirements and Goals .................................................................... 17

   Solutions Considered and Rejected .............................................................................. 20

       Generate the Pages Dynamically .............................................................................. 20

       Buy a Bigger Server .................................................................................................. 21

       Make the Site Generation More Efficient ................................................................. 22

Chapter 3 Project Description ........................................................................................... 24

   Three Part Solution Strategy ......................................................................................... 24

       1. Simplify the Website Generation Engine ............................................................. 24

       2. Implement a Central Controller ............................................................................ 25

       3. Implement a Distributed Deployment Model ....................................................... 26


                                                                 ix
   Solving the Graph Generation Problem ........................................................................ 29

   Languages, Environment and Straying From Standards .............................................. 30

      Ruby on Rails ............................................................................................................ 30

      Perl ............................................................................................................................ 31

      Web Services ............................................................................................................ 31

      Development Environment ....................................................................................... 31

      Hardware and Operating System Environment ........................................................ 32

      Parting From a Conventional Rails Application ....................................................... 32

Chapter 4 Project Implementation .................................................................................... 35

   Controller Web Administration .................................................................................... 35

      Teams List ................................................................................................................. 38

      Adding Teams to a Project ........................................................................................ 38

      Bug List ..................................................................................................................... 39

      Projections................................................................................................................. 40

      Online Help ............................................................................................................... 42

      Navigation ................................................................................................................. 43

      Authentication and Authorization ............................................................................. 44

   Definition of the Controller Schema and Migration of Legacy Data ........................... 45

      Schema Creation ....................................................................................................... 46

      Switching From a Perl to a Ruby Based Import Script ............................................. 50

      Mechanics of Importing Legacy Data ....................................................................... 52

   Generator Communication Mechanics ......................................................................... 52

      Generator Communication Code .............................................................................. 55




                                                                  x
       Error Management and Dealing With Rogue Generators ......................................... 56

   Graphing Implementation ............................................................................................. 58

       Graphing Implementation ......................................................................................... 59

       Generator Changes to Support Graphing .................................................................. 60

   Application Deployment ............................................................................................... 61

   NFS Configuration ........................................................................................................ 61

       Controller Configuration ........................................................................................... 62

       Supporting Generated Site URL Pass-Through ........................................................ 62

       Enabling Generators .................................................................................................. 63

Chapter 5 Summary and Conclusions ............................................................................... 64

   Comparison Metrics ...................................................................................................... 64

   Key Accomplishments and Lessons Learned ............................................................... 67

       Convention Over Configuration with the Team Model ............................................ 67

       ActiveScaffold - Record Level Policy & Permissions .............................................. 69

   Identifying the Current Logged In User In the Model .................................................. 71

       Messaging Between Controllers and Generators ...................................................... 72

       Addressing the Moving Projections Target .............................................................. 73

   Future Plans for eNoitfy ............................................................................................... 75

       Disabling a Site From Generating............................................................................. 75

       Predicting the Next Generation Time For a Site ....................................................... 75

       Partitioning eNotify Scale Based On Organizations ................................................. 76

       Augmenting security ................................................................................................. 77

Glossary ............................................................................................................................ 78




                                                                  xi
References ......................................................................................................................... 80

Appendix 1 Application Code .......................................................................................... 82

   Cascading Style Sheet (.css) ......................................................................................... 82

       public/stylesheets/custom.css.................................................................................... 82

   Embedded Ruby (.erb) .................................................................................................. 83

       app/views/administrators/edit.html.erb ..................................................................... 83

       app/views/administrators/for_project.html.erb ......................................................... 84

       app/views/administrators/index.html.erb .................................................................. 85

       app/views/administrators/new.html.erb .................................................................... 86

       app/views/administrators/show.html.erb .................................................................. 87

       app/views/bug_lists/for_project.html.erb ................................................................. 88

       app/views/graphs/show.html.erb............................................................................... 89

       app/views/layouts/admin_standard.html.erb ............................................................. 97

       app/views/login/index.html.erb............................................................................... 100

       app/views/owners/new.html.erb.............................................................................. 101

       app/views/people/edit.html.erb ............................................................................... 102

       app/views/people/show.html.erb............................................................................. 103

       app/views/projections/edit.html.erb ........................................................................ 104

       app/views/projections/edit_group.html.erb ............................................................. 105

       app/views/projections/index_for_bug_list.html.erb................................................ 106

       app/views/projections/index_for_team.html.erb ..................................................... 107

       app/views/projections/show.html.erb...................................................................... 108

       app/views/projects/edit.html.erb ............................................................................. 109




                                                                  xii
   app/views/projects/index.html.erb .......................................................................... 110

   app/views/projects/show.html.erb........................................................................... 111

   app/views/teams/for_project.html.erb ..................................................................... 112

   app/views/teams/new.html.erb ................................................................................ 113

   app/views/teams/show.html.erb .............................................................................. 114

Perl (.pm) .................................................................................................................... 115

   generator/Enotify/Generator/ControllerCommunicator.pm .................................... 115

Ruby (.rb) .................................................................................................................... 123

   app/controllers/administrators_controller.rb ........................................................... 123

   app/controllers/application.rb ................................................................................. 126

   app/controllers/bug_columns_controller.rb ............................................................ 127

   app/controllers/bug_lists_controller.rb ................................................................... 129

   app/controllers/generations_controller.rb ............................................................... 133

   app/controllers/graphs_controller.rb ....................................................................... 137

   app/controllers/login_controller.rb ......................................................................... 138

   app/controllers/owners_controller.rb ...................................................................... 139

   app/controllers/people_controller.rb ....................................................................... 142

   app/controllers/projections_controller.rb ................................................................ 144

   app/controllers/projects_controller.rb ..................................................................... 147

   app/controllers/teams_controller.rb ........................................................................ 150

   app/helpers/bug_columns_helper.rb ....................................................................... 153

   app/helpers/bug_lists_helper.rb .............................................................................. 154

   app/helpers/owners_helper.rb ................................................................................. 155




                                                              xiii
app/helpers/projects_helper.rb ................................................................................ 156

app/helpers/teams_helper.rb ................................................................................... 158

app/models/administrator.rb ................................................................................... 159

app/models/bug_column.rb ..................................................................................... 160

app/models/bug_list.rb ............................................................................................ 161

app/models/bug_list_downloads.rb ........................................................................ 162

app/models/bug_ownership_change.rb ................................................................... 163

app/models/bug_tool_url.rb .................................................................................... 164

app/models/generation.rb ........................................................................................ 165

app/models/generator.rb .......................................................................................... 166

app/models/owner.rb ............................................................................................... 167

app/models/ownership_model.rb ............................................................................ 168

app/models/person.rb .............................................................................................. 169

app/models/project.rb .............................................................................................. 171

app/models/projection.rb ........................................................................................ 181

app/models/scheduler.rb ......................................................................................... 184

app/models/scope_filter.rb ...................................................................................... 187

app/models/site_update_frequency.rb..................................................................... 188

app/models/team.rb ................................................................................................. 189

config/environment.rb ............................................................................................. 190

config/routes.rb ....................................................................................................... 192

db/migrate/002_create_projects.rb .......................................................................... 194

db/migrate/003_create_generations.rb .................................................................... 195




                                                        xiv
  db/migrate/004_create_generators.rb ...................................................................... 196

  db/migrate/005_create_bug_list_downloads.rb ...................................................... 197

  db/migrate/006_create_site_update_frequencies.rb ................................................ 198

  db/migrate/007_create_people.rb ............................................................................ 199

  db/migrate/008_create_bug_tool_urls.rb ................................................................ 200

  db/migrate/010_create_administrators.rb ............................................................... 201

  db/migrate/011_create_bug_lists.rb ........................................................................ 202

  db/migrate/012_create_ownership_models.rb ........................................................ 203

  db/migrate/013_create_projections.rb .................................................................... 204

  db/migrate/014_create_bug_columns.rb ................................................................. 205

  db/migrate/015_create_owners.rb ........................................................................... 206

  db/migrate/017_create_scope_filters.rb .................................................................. 207

  db/migrate/018_create_bug_ownership_changes.rb ............................................... 208

  db/migrate/019_create_teams.rb ............................................................................. 209

  lib/enotify_ldap.rb ................................................................................................... 210

  lib/thesis_importer.rb .............................................................................................. 213

  lib/user_info.rb ........................................................................................................ 226

  script/import/project.rb ........................................................................................... 227

  script/thesisdoc/generate_doc.rb ............................................................................. 228

YAML Ain't Markup Language (.yml)....................................................................... 233

  config/database.yml ................................................................................................ 233

  db/constant_fixtures/bug_columns.yml .................................................................. 235

  db/constant_fixtures/bug_ownership_changes.yml ................................................ 241




                                                           xv
db/constant_fixtures/bug_tool_urls.yml ................................................................. 242

db/constant_fixtures/ownership_models.yml ......................................................... 243

db/constant_fixtures/scope_filters.yml ................................................................... 244

db/constant_fixtures/site_update_frequencies.yml ................................................. 245




                                                  xvi
                                               List of Figures



Figure 1 eNotify Website Generator Work flow ................................................................ 8

Figure 2 Current Refresh Time for Sites on Legacy eNotify Servers............................... 11

Figure 3 Example eNotify Graph ...................................................................................... 13

Figure 4 Time to Complete Tasks - Legacy System vs Goals .......................................... 17

Figure 5 Proposed eNotify Deployment Model ................................................................ 27

Figure 6 eNotify Web Administration Project Summary ................................................. 36

Figure 7 eNotify Web Administration Edit Project Settings ............................................ 37

Figure 8 Project Summary with Setup Instructions .......................................................... 37

Figure 9 eNotify Web Administration Teams List ........................................................... 38

Figure 10 eNotify Web Administration Project Bug Lists ............................................... 40

Figure 11 eNotify Graph Showing Projection and Historical Bug Data .......................... 41

Figure 12 eNotify Web Administration View and Edit Projections ................................. 42

Figure 13 eNotify Web Administration Online Help ....................................................... 43

Figure 14 eNoify Web Administration Navigation Hierarchy From Team Page ............. 44

Figure 15 eNotify Web Administration: Authorization Restricted................................... 45

Figure 16 Controller Schema ............................................................................................ 47

Figure 17 Generator and Controller Communication Flow .............................................. 53

Figure 18 eNotify Graph Showing Projection and Historical Bug Data .......................... 58

Figure 19 Time to Complete Tasks - Legacy System vs Goals vs Results....................... 65




                                                            xvii
                            Chapter 1 Introduction



       This project is an extension of a bug tracking application called eNotify, also

referred to as the legacy application. It generates a website showing all of the teams and

individuals involved in a software project and which bugs each person own. eNotify is

used by managers to track and drive bug management processes, and by individuals to

determine exactly what bug-related work they have to complete.

       eNotify was developed as part of Harvard Extension School course CSCI E-13,

“Practical Perl,” and deployed at my place of business. The original launch of eNotify

met with substantial success and the demand for the application’s services has far

exceeded the capabilities of the original design resulting in substantial support workload

for the legacy operation’s team and frustration for the end users.

       The aim of this project is to develop and implement a solution to the legacy

shortcomings and deliver the following three benefits:

           1. The application is able to meet end users demands for current data;

           2. The support demand on the operations team is significantly reduced;

           3. The application has been re-engineered such that the existing team with

               minimal effort can enhance it.

       This document is broken into 5 chapters. Chapter 1 (this introduction) contains an

explanation of the need for this bug tracking system, an explanation of the legacy



                                             1
application, and a review of its shortcomings. Chapter 2 details the goals of this project,

criteria for success and solutions that were investigated and not selected. Chapter 3

provides the description of this thesis project, detailing the solution that was chosen, the

high level design of the new application, and discussion of the reasoning for decisions

made in the design. Chapter 4 covers the details of the implementation including specifics

on each of the five phases of the project development and deployment. Chapter 5

describes the validation of successful requirements implementation. It also covers the

major achievements and lessons learned during this project as well as future plans for the

application.


                           Bug Tracking in a Large Company


       Large engineering organizations with thousands of projects and tens of thousands

of engineers often have unique bug tracking processes when compared with the majority

of smaller engineering organizations in the world. The process a large company uses to

govern bugs is a core part of its DNA and can be a differentiator amongst competitors.

Where some small organizations work well with the concept of assigning a bug to a

single person and having that person see the bug through the bug’s life cycle, in a large

company ownership of a bug usually spans more than one person. In a large engineering

organization, bug ownership is distributed to a number of people each specializing in a

specific part of the bug resolution. For example:

              New bugs need to be triaged and assigned. The development-engineering

               manager of the software component to which the bug belongs owns this

               task.



                                              2
              Assigned bugs need to be fixed. The engineer to whom the bug is assigned

               owns this task.

              Resolved bugs need to be verified by someone other than the engineers

               who fixed them.

              Bugs require customer facing documentation that describes the symptoms,

               steps to reproduce, and any known workarounds.

       There are many other bug states and ownership scenarios. Even so, defining

ownership rules can be a simple task. The challenge comes in getting a large group of

individuals to understand the ownership rules, interpret them the same way, and realize

they need to take action on a bug without being reminded. Without an effective bug

tracking application applying ownership rules that explicitly display the owner of bugs

for specific tasks, one of two scenarios occurs. Either bugs are ignored or the

organization invests a substantial amount of effort in manually tracking ownership of

bugs and notifying people of their open tasks such that bugs are not ignored.

       In my place of business, the corporate bug tracking system, from which eNotify

gets its bug data, does not provide the concept of bug ownership. Changing the corporate

bug tracking system to support ownership concepts is not an option due to cost. While

specific details of the corporate bug tracking system are confidential it is possible to share

the following details. At present the corporate bug tracking system contains tens of

million of records and is tightly integrated with other company functions including but

not limited to software patching, manufacturing, development operations, customer

support and employee compensation. Additionally, over 10,000 people use the system

daily. The cost of retraining those individuals to compensate for the addition of

                                             3
ownership concepts to the workflow of the bug tracking user-interface was estimated at 8

million dollars, by the training organization. Figures in this range are typically non-

starters if the person recommending a software change does not own or operate the

system in question. Thus, at the start of this project replacing or changing this bug

tracking application to support ownership was not a viable business decision.



                         Legacy eNotify Application Overview


       As a final project for CSCI E-13 “Practical Perl” I designed and developed an

application called eNotify that automated and streamlined my company’s internal bug

management processes. The pre-existing process was manual and required consistent

communication and action from all involved/stake-holding employees in order to

maintain an accurate view of the current bug status. The bug management process often

broke down as a result of inconsistent communications resulting in missed delivery dates

and software release delays.

       An example of this occurred 6 years ago during a large product integration

project. Four products were being combined to produce a single feature-rich application.

The project personnel consisted of over 300 engineers making up 22 teams. This was

their first time working together and most of the work consisted of making the four

products communicate with each other. As a result it was often unclear which of the

products was responsible when a bug was found.

       Bruce (not his real name), the project manager, held a weekly project status

meeting where the managers of the 22 teams would provide status reports. Bruce made it

clear that the project had to resolve over 800 bugs before the product could ship in six


                                             4
months. Given the 300 engineers available this did not seem like an overwhelming task.

A massive spreadsheet with a list of the bugs was manually maintained. Each Monday

managers would report progress on reducing their bug count, but the overall 800

outstanding bug count was not going down. Due to the complexity of the integration

task, the managers were mistakenly underestimating the number of bugs they needed to

fix by assuming it was another team’s responsibility.

       One month before the expected ship date the outstanding bug count stood at just

under 800 bugs. Bruce called an open-ended meeting and forced the managers to go

through each bug and agree on its ownership. Thirteen hours, and a substantial amount of

arguing later, the spreadsheet was up to date. With a clear view of the work in front of

them the managers agreed they would need an additional year to complete the project.

       The following year Bruce implemented a strict set of rules governing spreadsheet

updating. Each team had a spreadsheet and was expected to keep it up to date with the list

of bugs they owned. Weekly meetings were held, and the majority of the engineering

organization’s time was spent keeping the bug spreadsheets up to date and

communicating changes to engineers so they would know which bugs to fix. The process

was extremely error prone and at the end of the one-year project extension, the bug count

stood at 700. The project was extended for a third year. The leadership of the

organization began to realize that their bug management process had failed. With a new

deadline looming and no reason to believe they would achieve different results with the

same process, they were looking for a new way to manage their project.




                                            5
                                   What eNotify Does


       eNotify provides a mechanism for implementing a consistent ownership model

across bugs in a project. It eliminates the manual overhead of tracking bugs and provides

an automated mechanism to consistently communicate the specific work owned by each

individual to that individual and the rest of the team. A project leader uses the eNotify

web administration to identify the people working on a project and the lists of bugs that

should be tracked. Using this information and a predefined set of ownership models

eNotify generates a static website of simple action-oriented pages providing the project

managers, teams and individual contributors with a clear view of the bugs they have and

what they should do with them.

       eNotify was introduced during the third year of Bruce’s integration project. It

immediately cleared up all questions of bug ownership, eliminated the need for bug

spreadsheets, gave each engineer a clear list of bugs they needed to fix, and allowed the

engineering organization to shift its focus from bug tracking mechanics to the fixing of

bugs. When Bruce’s product shipped, on time per the reschedule, it had 12 known, low

severity bugs and everyone knew exactly who owned those bugs.


eNotify Server Basics

       The original eNotify was designed to reside on a single server. It is capable of

supporting bug tracking for 1 to ~10 projects in the form of a project website henceforth

referred to as a site. Configuration for each project’s site was managed through a web

portal and the configuration settings were stored on disk in flat files in a number of

formats (XML, property=value and CSV). The goal with this storage mechanism was to



                                             6
keep the application simple and to allow for manual manipulation of the configuration

files if/when the administration console had a bug.


Website Generator Work Flow

       The eNotify site generation process is written in Perl (Perl Foundation, 2002) and

is launched by a recurring cron program (Stevens, 2002) in Linux or Scheduled Task

(Morimoto, Noel, Droubi, Mistry, & Amaris 2008) in Windows. The task checks for a

running version of eNotify and if one does not exist it runs the eNotify website generator.

Websites are generated following the workflow diagrammed below. Per Figure 1, the

eNotify process reads the list of projects that need to be updated and updates them in

series. Once a site is generated, the HTML (WC3, 1995) files that make up that site are

accessible via a web server that serves the static pages from the server’s disk.




                                             7
                      Figure 1 eNotify Website Generator Work flow


Scaling and Server Limitations

       Given the serial nature of project site rendering, theoretically the number of

projects supported on a server is limited by the availability of disk space required for the

generated project websites. In reality the time it takes to generate the series of websites

combined with the end user’s demand for updates is the practical limiting factor. Each

project website contains information that is critical to the day-to-day operations of a

project. As a result, teams want their project websites updated as frequently as possible.




                                              8
The time it takes to generate a project website is based on the number of objects (web

pages and graphs) that need to be generated. Thus generation time is proportional to:

        <Constant rendering time> + ( <#users> * ( <#bug lists> + <#bugs
        per list> ) )

        As the number of users, bug lists and bugs increase, so does generation time.

When run in series, the duration between site updates is equal to the sum of the

generation of all sites on that server. Since project website generation and page hosting is

tied to a single server, and the variables (users, bug lists and bugs per list) are controlled

by the users, it is difficult to keep any given group of sites from exceeding the capacity of

their hosting server. When a server runs out of capacity, project website generation

frequency drops and project teams are unable to get their bug ownership data in a timely

fashion. In extreme cases sites take more than 24 hours to refresh.

        An example of eNotify capacity overload occurred in the months following the

successful completion of Bruce’s integration project. During the project review, eNotify

was identified as a best practice and received significant publicity. As a result, I received

numerous requests from project managers to create eNotify websites for their projects. At

the time, the eNotify server capacity and site refresh rate had never been tested. Because

Bruce’s project website was the only one on the server it refreshed without delay. Bruce’s

website took approximately one hour to generate, and that frequency was acceptable to

Bruce and his engineers. As new project websites were added to the eNotify server and

project managers began adding teams and bug lists with large numbers of bugs,

individual project managers began to see delays in their site updating. The problem

became clear when a project manager named George added 60 teams with over 600

engineers and more than 40 bug lists to an eNotify project. George’s site took 6 hours to


                                              9
generate and other project administrators immediately started complaining that their

eNotify websites were not refreshing fast enough.

       As more projects were added to the eNotify server it was not uncommon for a site

to go more than 24 hours without a refresh. The engineering organization had come to

rely on eNotify to provide task lists to engineers. Delays in eNotify website refreshes

translated into lost engineering cycles as new bugs were entered into the bug tracking

system but were not identified by eNotify websites that had not yet refreshed.


Legacy Application Shortcoming: eNotify Falls Victim to Its Own Success

       The legacy eNotify application succeeded in streamlining our bug management

process and within a year of Bruce’s project completion eNotify was adopted across the

business, requiring the deployment of 6 eNotify servers. The addition of new servers was

driven by end-user’s requirements that sites must be refreshed in less than 2 hours.




                                            10
            Figure 2 Current Refresh Time for Sites on Legacy eNotify Servers

       Figure 2 shows time between site refreshes on each eNoitfy server. Servers 1, 4

and 6 each meet users requirement for refresh times of less than 2 hours, and project

managers using those servers are quite happy. Server 2 has recently exceeded the two-

hour mark and servers 3 and 5 are severely overloaded with large projects that result in

long delays for websites to refresh. From the perspective of the operations team and the

end users, these servers are over capacity and the operations team is rejecting requests to

add new projects until this thesis project is put into production.

       Limitations of the legacy application result from the fact that eNotify sites are

bound to a server running an instance of eNotify that is responsible for generating sites



                                              11
for many projects. All other sites see the time it takes eNotify to generate one site as lost

refresh time. The task of load distribution, currently achieved by manually copying a

site’s metadata from one server to another server falls to the operations team. The process

is time consuming and error prone.

       Due to inconsistencies between the installations on two eNotify servers, the last

manual load distribution required a 9 day effort by an operations team member, during

which time one eNotify website was offline for 70% of the time. While it is difficult to

quantify the productivity impact on the end users it was clear that the project manager

was extremely unhappy.

       The major issues faced are the result of the basic design assumption that an

instance of the application is bound to a single server. Those issues are:


Sharing Configuration Restrictions

       There is no capability to share site configuration, user lists or bug lists across

servers. Project administrators want the ability to share or copy configuration from one

project to another but are unable to do so when projects reside on separate servers. In

order to share configuration, organizations must co-locate projects on a single server

which, as we saw in the case of George’s project, can results in over utilization of that

server’s resources, reduction in project website update frequency, and unhappy

customers.


Inability To Scale With Demand

       Because the generation of a project website is tied to a specific server and a

project website’s generation time is proportional to <Constant rendering time> + (



                                              12
<#users> * ( <#bug lists> + <#bugs per list> ) ) it is possible that the configuration of

additional bug lists in a project, or a spike in the number of bugs being tracked for a

project, will result in significant changes in the time it takes for a project’s website to be

generated. This problem is exacerbated by the operation team’s lack of control over the

eNotify site metadata. Project managers are able to add bug lists and teams to projects on

demand, which can dramatically impact the generation time of a site and the delay for

other sites to refresh as was the case with George’s addition of 60 teams, 600 engineers,

and 40 bug lists to his project.



Graph Rendering Consumes Disproportionally Large Resources

       eNotify graphs show three pieces of information:


                  The projections set for this bug list

                  The bug counts as recorded by the generator for past dates

                  The current bug count




                              Figure 3 Example eNotify Graph




                                              13
       Legacy graph generation uses the Perl package GD:Graph (Verbruggen , 1999)

and generates one graph per bug list for each individual, each team and the cumulative

project bug data. Users insist that graphs always need to be available, but according to

the web server logs, views of the graphs are infrequent as compared to the number of

times the graphs are generated.

       Generating these graphs is processor intensive and represented a large portion of

the generation time and processing load. In order to improve the site refresh frequency a

trade off was made in the legacy application. Graphs are only generated once a day. This

often reduces generation time by more than half, but it also results in out-of-date

information in cases where the projections are changed.

       Projections are the predicted bug counts that a team expects to have at any point

in time. Projections are used as a tool for setting a bug count reduction goal and then

tracking to that goal. Each team’s manager is responsible for entering their team’s

projected bug counts into eNotify though a web interface.

       When a team manager changes their projections the changes are not immediately

reflected on the graph. The manager must wait until the graphs generate the following

day in order to see the change. This is often problematic for project planning purposes

and system administrators frequently receive requests from managers to manually trigger

a regeneration of graphs. These events increase system load, reduce site update frequency

and create manual work for the administrators.




                                             14
Expensive Feature Development

       The administration console is intended to provide an interface for users to manage

their project’s website configuration. The prototype administration evolved from an

infrastructure using Perl CGI (Waterman, 1999) and Template Toolkit 2 (Wardley, 2007)

in a non Model View Controller (Sun Microsystems, 2002) like framework. In order to

make a single change to functionality or display, code level edits are required in multiple

places: the objects responsible for reading and writing to the flat file storage, accesses

and mutates on the objects used in the admin, and in 2 layers of the admin display. A

change in one part of the application could easily require changes in three other places to

keep the application from breaking. As a result, the legacy generator remained untouched

for years and due to the high cost of change in this environment few updates or feature

enhancements are affordable.



New Site Management & Scaling Optimization

       In order to manage new site addition to the servers, a dedicated administrator

from the operations team fields each request for a new site and manually balances the

server load by guessing how much load the site will eventually cause and choosing a

server that will likely be able to support this load. Because most of the variables

determining load are unknown at the time of site creation, this is an inefficient and

ineffective way of managing load. In addition to the challenges of guessing load, the

manual effort required from the operations team to create sites often results in a 3 to 4

day delay when a project manager requests a new eNotify site.




                                              15
Project Administrator Change Requests

       Each legacy eNotify project supports a single administrator. This administrator is

typically the project manager and is responsible for configuring their project’s teams and

bug lists. Only the server administrator can change the project administrator, as it

requires manual changes to the project’s configuration files. Requests for project

administrator changes are frequent as projects evolve and project administrators change

roles or go on vacation.

       Overall, the solution has reached a support tipping point. The six servers require

constant care to manage their environment and the cost of configuring a new server is on

the order of four engineering days. While the business can’t afford to take eNotify

offline, it would clearly benefit from an architectural renewal that would reduce the

support costs, simplify the mechanism for enabling sub 2-hour site refresh rates and allow

for future innovation of the business process.




                                             16
           Chapter 2 Solution Requirements and Goals



       The requirements for this project were identified through conversations with the

operations team and the end users. The requirements are focused on reducing or

eliminating time spent waiting for eNotify project website generation or costs associated

with manual load distribution.




                Figure 4 Time to Complete Tasks - Legacy System vs Goals




                                            17
There are six requirements designed to drive the savings as shown in Figure 4:

   1. Provide the ability to increase a site refresh frequency with zero end user

       down time. As described in chapter 1 this process varies, but the last

       migration resulted in approximately 9 days of down time.

   2. Eliminate the site migration operations cost. This is related to requirement

       1, but focuses on manual labor cost to the operations team, which is also

       variable and most recently resulted in a 9 day effort.

   3. Reduce the operations labor cost for adding capacity. The current cost of

       adding and installing an eNotify server is 2 business days for one

       engineer.

   4. Eliminate the 3-4 day delay for new site creation after a project manager

       requests a site.

   5. Eliminate the manual labor cost for the operations team associated with

       responding to new eNotify site requests and existing site maintenance

       requests. Cumulatively these tasks cost the operations team approximately

       4 person days of effort a month.

   6. Eliminate the 24-hour delay for graph updates. Graphs, including

       projections, should update at least as frequently as eNotify websites

       without incurring additional rendering time at the generator.




                                    18
       Several solutions have been considered for resolving the issues with the existing

prototype. The following are the criteria for success that were used in choosing a solution

for this problem. Ties were broken by the impacted criteria with the lowest number.

       1) Requirements: Address the six requirements identified above.

       2) Cost: The solution must align with existing business return on investment

standards. In our current business climate there is a push to leverage existing

infrastructure as opposed to investing engineering resources to achieve a goal without

that internal infrastructure. The cost of low-end hardware and virtual machine based

systems has dropped significantly. When attempting to achieve faster processing and

scalability it is cheaper and more politically palatable to leverage low-end hardware or

virtual machines then to invent a newer faster processing algorithm.

       3) Simplicity: The solution will be handed off to a team that has little time to

manage it. The application needs to be relatively self sufficient and conceptually simple

to understand.

       4) The right technology: The initial prototype was designed using Perl because

there was an expectation that the complicated part of the application would be in parsing

the configuration and bug data. While this turned out to be true, using Perl for the web

administration became excessively complicated. The skill set of the support team is not

well suited to maintaining a complex Perl based web application. For that reason, if there

is a need to rewrite the web administration, the technology choice for the re-

implementation needs to be designed for web application development and must fit the

skill set of the team that will support the application.




                                              19
                           Solutions Considered and Rejected


        The following solutions were considered and rejected after failing to meet one or

more of the above criteria.


Generate the Pages Dynamically

        From a design perspective it makes sense to question the a priori nature of the

page generation. It’s likely that a large majority of the generated pages are never viewed.

In the original design I decided to implement a priori page generation due to the

underlying data source and eNotify’s original business goals.

        The underlying bug data source is a flat file storage mechanism accessed via CGI.

Response times on queries range from 30 seconds to several minutes and are returned in

text format. The meta-data required to apply an ownership model, the cornerstone of

eNotify’s functionality, is not available via the bug data source. Thus, in order to create a

combined data model the bug data would need to be imported into a new database and

merged with the ownership model meta-data.

        While on demand page generation is possible there are several issues that prevent

its implantation in this project.

    1. At the time the legacy application was written, scalability was not a design

        concern. Rather than adding the complexity of replicating the bug data in a

        relational database, generating pages a priori was a simple solution given the

        toolset I had available.

    2. Leveraging the existing data source allows eNotify users to utilize their

        knowledge of the existing proprietary bug query language for defining bug lists.


                                             20
       Thus no training or mapping is required and bug queries work across corporate

       tools.

   3. There is a multi-year corporate effort under way to migrate the existing bug

       database to a model that would support on demand page generation. In this

       context eNotify is seen as a 4 to 5 year stopgap tool.

   4. The existing mechanism for moving bug data from the source through the

       ownership model and into the UI is extremely stable. It has been recognized as a

       best practice for bug data sourcing.

   5. In light of the previous points the business would like to limit its development

       investment and instead focus on reusing the existing code-base wherever possible.


       On demand page generation is clearly the purest technical solution. In this case

that solution is sub-optimal when combined with the known business factors.




Buy a Bigger Server

       A 64 bit quad processor server with a 500GB RAM disk and 64GB of memory is

capable of running all of our existing eNotify websites in parallel without minimal code

changes to accommodate parallel processing. This solution allows us to meet our

immediate scaling needs and centralize all sites onto a single server, thus eliminating the

multiple URLs and inability to share configurations. There are four major issues that

eliminate this strategy as a viable option:




                                              21
       1)      The business is unwilling to foot the $24,000 bill to purchase one of these

                  servers.

       2)      One server would represent a hard to replace single point of failure.

       3)      At our current pace, we would surpass the capacity of such a machine

                  within 6 months.

       4)      This solution does not address the high cost of adding features via the

                  existing administration framework.


Make the Site Generation More Efficient

       The existing system could be optimized to make better use of the existing

resources. Smarter algorithms for applying ownership and rendering pages could make it

possible to extend the scalability of the existing servers. It would be possible to move to a

dynamic page generation model where pages were rendered only when a user requested

them. More efficient or dynamic generation algorithms were considered during the

original prototype phase as well as during this latter scaling resolution phase. This avenue

is appealing for two reasons. First it eliminates computer cycles being wasted on pages

that are never viewed and second it provides for an interesting thesis topic. From the

business perspective it has some major issues.

            1. The high engineering cost of rebuilding the infrastructure for a working

               system from the ground up is hard to justify in the presence of other

               options.

            2. The solution complexity would increase dramatically which typically

               translates into higher support and maintenance costs.


                                             22
           3. As load increases, processing power for page generation will continue to

               be an issue. This could be addressed through distributed web server load

               balancing, but that only increases the complexity of the solution.

           4. There is a simpler and cheaper solution.

       Each of these solutions was rejected due to its inability to meet one or more of the

project requirements. The following chapter details the selected solution.




                                            23
                       Chapter 3 Project Description



       The solution chosen to resolve the shortcomings of the legacy

application involves refactoring both the application and its deployment model. This

approach requires re-architecture of the administration web application, retaining the site

generation logic and leveraging existing corporate infrastructure to enable a more flexible

deployment model. There are three distinct steps that when viewed as a whole provide a

cheap, scalable, and flexible solution.


                              Three Part Solution Strategy



1. Simplify the Website Generation Engine

       The goal of this step is to take the site generation capability of the existing system

and decouple it from the configuration and business logic. The legacy site generator

consumes project configuration data from a variety of flat files saved by the

administration web server, to its local file system, and then renders generated pages to

that same file system. The site generator will be altered such that it polls a web service

for site update instructions and configuration data and renders pages to a NFS (Network

File System (Red Hat, 2005)). This will allow the site generator to be hosted on a

separate server from the administration server. Site generator servers are configured to

mount an additional NFS drive containing the generator code such that the complete

setup of a generator consists of mounting a network drive and configuring a cron job to



                                             24
call the generator code. Multiple site generation servers can be brought online to expand

the scaling capacity of the system.

       This solution essentially creates a cloud of computing power that is applied to all

of the eNotify sites. By decoupling site generation from a specific server the concept of

server specific refresh rates, as seen in Figure 2, are eliminated. Load distribution is

automatic across multiple generators. This eliminates the need for manual load

distribution, the resulting site down time, and operation labor costs associated with site

migrations across servers.


2. Implement a Central Controller

       The existing administration console will be retired and re-implemented as part of

a centralized administration and configuration management system. The intent is to use

the Ruby on Rails (Hansson, 2005), full-stack (Bennett, 2006) MVC (Model View

Controller) web framework as it allows for rapid feature development, and is a

technology with which the support team is familiar. The central controller will provide

all of the features of the previous administration console as well as:


      A stable and flexible web interface for creating and editing project configuration
       data
      A single repository for all site data
      The business logic that determines when sites update and in which order
      A set of web services (World Wide Web Consortium, 2007) that provide:
           o   Access to the business logic in the form of a poll-able service that gives
               generators instruction to generate a project website
           o   All the configuration information needed by a website generator to build a
               project website
           o   A mechanism to report acceptance of a site generation task


                                               25
           o   A mechanism to report website generation progress as well as error and
               success data
      A mechanism to manage and track numerous generators
      Support for multiple site administrators per project
      A pass through mechanism for generated page requests such that requests for both
       the web administration, and the generated project pages, can be served up from
       the same domain.


       This creates a central point of administrator and end user control for the system

housed on a single server.


3. Implement a Distributed Deployment Model

       The deployment model is the linchpin to addressing the requirements while

making this solution operationally scalable. The diagram below shows a high level view

of the intended eNotify system deployment.




                                             26
                       Figure 5 Proposed eNotify Deployment Model




       Site generators are hosted on a set of identical virtual machines that are easily

replicated. The website generators are configured with a repeating cron job to call out to

the central controller web service requesting the name of the site to update. Each

generator has an NFS mount to a shared network drive, which it uses for accessing the



                                             27
eNotify generator application and for storing generated web pages and downloaded bug

data.

        The central controller web application is hosted on a dedicated server. The central

controller manages all the business logic determining the order of sites updating. When

queried by a generator, the central controller determines which site should be updated and

returns that site’s configuration data to the generator. Site update order is based on a

number of criteria including desired refresh frequency, last update time, user defined

priority of each site, and administration override priority.

        Users access both administration as well as generated pages through the central

controller. Project site administrators use the central controller web application UI to

manage their projects. The central controller also has a web server configured to serve up

generated pages from the mounted NFS drive. This provides a single URL for accessing

both the administration UI and generated project websites.

        From a cost perspective this solution benefits from the ability to leverage existing

hardware resources as well as a smart use of engineering investment. The NFS is already

in place at my company and is a cheap storage solution easily capable of scaling to our

future needs. By simplifying the site generators to be processing drones, we are able to

leverage any cheap hardware or virtual machine by simply mounting the network drive

containing the generator application, and letting the central controller manage its

behavior. The development of a new administration web application in a Ruby on Rails

MVC framework allows for reduced cost of future development while not overburdening

engineering with an excessive development task. Ruby on Rails (Hansson, 2005) is

specifically designed for development of web applications and its use of intelligent


                                              28
defaults and standards significantly reduces development time over other languages and

frameworks such as PHP (PHP Group, 2005), Java/Hibernate (Red Hat Middleware,

2006) and Perl/Mod CGI.

       While the solution leverages a number of technologies, each one is used in a

relatively straightforward way. The decentralized nature of the solution allows us to

easily scale the application in the future simply by cloning one of the existing site

generation drones. The central controller provides a common point of entry for access to

generated eNotify websites as well as system monitoring and maintenance control. The

solution benefits from component level simplicity that should translate into reduced

maintenance costs.


                        Solving the Graph Generation Problem


       While the majority of the legacy shortcomings are addressed through the three-

stage re-architecture and deployment change, the graphing issue remains. The two issues

with graphs are the:


   1. Extremely high generation to use ratio.
   2. Out of date data when projections are changed.


These issues are addressed by eliminating the graph-rendering step from the generator

and rendering graphs on demand via the controller. The controller has the projections in

its database, thus projections are updated each time the graph page loads. This has the

benefit of eliminating all generator processing time for graph rendering, and eliminating

the class of requests for system administrators to manually override graph generation.



                                             29
                Languages, Environment and Straying From Standards



Ruby on Rails

       The central controller is implemented in Ruby on Rails using a MySQL database

(Sun Microsystems, 2009). Ruby on Rails is a combination of the Ruby (Thomas, 2005)

language coupled with a full stack MVC framework called Rails. Ruby on Rails

simplifies the development task by nearly eliminating the need to repeat configuration

steps in the different parts of the application stack. While the designers of Ruby on Rails

favor convention over configuration they still give full control to the developer to

override convention when necessary. The conventions used in Ruby on Rails are

extremely well thought out and will save time and effort in future development.

       The decision to use the Ruby on Rails framework with MySQL was based on two

main factors.

           1. I’ve developed a preference for using Ruby on Rails based on experience

                developing web applications in C, Perl, Java and Ruby on Rails.

           2. The operations team has a strong skill-set and existing application

                experience with Ruby on Rails and MySQL. Their ability to successfully

                support and enhance the application in the future without adding

                substantial cost is enabled by use of a programming language and tools

                with which they are already familiar.

       These factors make it an excellent technology choice for the development of a

web application such as the central controller.




                                             30
Perl

       Perl development is necessary for the conversion of the existing prototype from a

full web app to a drone generator as well as to migrate the existing configuration data into

the central controller database. During the proposal stage this decision was based on the

assumption that it would be easier to carve a drone generator out of the existing eNotify

code, and build a web service based configuration data fetcher in Perl, than it would be to

rewrite the generator in Ruby. The basis for this assumption was the existence of a

functioning generator and proved to be true during development prototyping.


Web Services

       All remote programmatic communication is implemented using standard web

services. REST (Hass, 2005) was chosen as the communication protocol as it was

essentially made available for free by the Ruby on Rails stack.


Development Environment

       The original controller development was done on Windows using RadRails

(Aptana, 2006). I experienced numerous problems with the Aptana IDE and logged two

bugs that the Aptana developers are in the process of fixing. Mid way through

development I switched the controller development environment to OS X and used

TextMate (MacroMates, 2009). The minor updates to the generator code were done in

jEdit (Pestov, 2004) as I was already familiar with Perl development in that IDE.




                                            31
Hardware and Operating System Environment

       Both the central controller and the generators are deployed on a farm of Red Hat

Linux Server images running on VMware ESX servers (VMware, 2007) in our corporate

data center. The standard Red Hat (Red Hat, 2005) virtual machine has NFS access to

our corporate file system enabled by default.

       Due to the simplicity of the generator deployment, nearly any Linux based

machine can be used as a generator. While virtual machine images are the easiest to

manage, the generator’s simplicity allows us the option to use any unused low-end

servers. This provides flexibility to use any machines that are available to scale the

system in the future. The collection of available generators act as a cloud of computing

resources available to the application and easily scaled up and down to meet the needs of

the system.



Parting From a Conventional Rails Application

       The conventional Ruby on Rails approach to solving the design problem is to put

all of the bug data into the database and build a fully dynamic web application. This was

not the approach taken. In this solution, only the eNotify site metadata is stored in the

controller’s relational database while the bug and generated data are stored in flat files,

per the legacy generator design. The decision to separate the data storage across

paradigms, in clear opposition to the conventional Ruby on Rails approach, was a critical

design decision and warrants explanation.




                                              32
Challenges With a Relational Approach to Generator Data

       Storing eNotify site data in a MySQL relational database means not only

replicating the corporate bug data, but also storing all of the historical data needed to

generate the graphs. The historical data is a snapshot of the state of the bug counts,

through the lens of eNotify ownership models, at given points in time. Due to a limitation

in the corporate bug tracking system, historical data cannot be reproduced and must be

recorded in real time by eNotify. This puts a large data storage burden on eNotify. Based

on the existing graphing requirements the number of rows required for storing historical

bug count data for 1 year is roughly 11 billion rows.

       (~100 active projects) * (~300 owners per project) * (~1000
       active bug lists ) * (1 year of active data ~365 days) = ~11
       billion rows

       Excluding the need to replicate the bug database, the requirement to store 11

billion additional rows of data each year in a new format represents a significant technical

challenge for a single MySQL database which has a limit of approximately 4 billion rows

per table (Zawodny, 2009). Sharding (Hibernate, 2009) provides a technical solution to

the problem by allowing a single table’s data to span multiple MySQL databases, but

brings with it complexity that the operations team is not prepared to manage.

       Investigation of other solutions, such as storing each site’s data in separate

databases or leveraging the capabilities of a mass storage non-relational database such as

Apache CouchDB or Apache Hadoop all fail to meet the goals, described in chapter 2, in

two key ways:


   1. The Right Technology: It requires a database and application scaling skill set

       that is not part of the existing operations team.



                                              33
    2. Simplicity: It requires substantial changes to, or even replacement of, the

        generator code, which increases both the complexity, and the scope of the solution

        without providing value in terms of the projects requirements or goals.




        Additionally a standard Ruby on Rails implementation with all of the data in the

database dramatically increases the scope of the development effort on this project. That

increase in scope includes both the application and database scaling considerations as

well as the development effort associated with alternating the generator. Given that this

is a mission critical application and the generator has been generating flawless pages for

over 5 years, there is a substantial aversion to adding risk to the system by re-writing its

core functionality. In this case, re-implementing the generator solution using the

conventional Ruby on Rails relational database approach would have resulted in a case of

over engineering a solution to a problem that did not exist.

        The solution using a server farm of cloud based generators and an NFS data store

provides a solution that leverages the robust parts of the existing application, the physical

resources we have available, and the core competencies of the support team that will own

this application in the future.




                                              34
                    Chapter 4 Project Implementation



The project implementation effort was made up of six major tasks:


   1. Controller web administration design and implementation
   2. Definition of the controller schema and migration of legacy data
   3. Communication between the generators and the controller
   4. Generator support for the controller
   5. Dynamic graphing implementation
   6. Deployment


The project’s progress was tracked on a blog at http://thesisdev.blogspot.com/.


                             Controller Web Administration


       The controller web administration user interface, henceforth web administration,

provides an interface for project managers to create new eNotify projects and administer

the ones they own. The logical starting page for the web administration is the project list

page which contains lists of the projects currently supported by eNotify, a summary of

each project’s configuration and links to view more detail.




                                             35
                  Figure 6 eNotify Web Administration Project Summary

        From this page users also have the ability to create a new eNotify project, edit a

project’s global settings (shown in figure 8), and view a project’s bug lists, teams and

administrators. For example Bruce, the project manager, would come to this page to set

up a new project. He would click on “Create New” at the top right of the projects table,

and would enter project metadata such as the name of the project, the frequency with

which the site should update, and the start date that should appear on eNotify graphs for

this project.




                                             36
                Figure 7 eNotify Web Administration Edit Project Settings



       Once Bruce saves the project it is created in the database, but additional data is

required before a generator can generate this project’s website. The status for the project

displays in red as “Setup Required” on the Project list as show in Figure 8. Clicking on

the “Setup Required” status brings Bruce to the project’s summary page where

instructions are provided to walk him through the remaining setup steps.




                    Figure 8 Project Summary with Setup Instructions




                                             37
Teams List

       One of the steps Bruce must complete to set up a new eNotify site is the definition

of the people that can be bug owners on the project. eNotify supports the concept of a

single level team hierarchy for owners on a project. Thus, an eNotify project has a set of

teams and each owner belongs to a team. Teams are identified by the team manager’s

name. A manager can only manage one team per project.

       Bruce accesses the Team List page by clicking on the “Team Count” column in

the server summary. The Team List provides a simple list of the teams in this project.

From the Team List page Bruce can click on a manager name to see the list of owners on

a team. As this project’s administrator Bruce has the unique ability to add a new team or

delete an existing team using the links on the right side of the Teams List table




                     Figure 9 eNotify Web Administration Teams List


Adding Teams to a Project

       To add a new team to his project Bruce clicks on the “Create new team” link. He

is prompted for the username of the manager of the team and has the option to have that

manager’s employees, as defined in the corporate LDAP (IETF, 2006) server, added to

the team. The eNotify controller is connected to the corporate LDAP system to provide




                                             38
automated checking for username validity and to enable the auto-population of manager’s

employees into an eNotify team.

       Usernames are the unique identifier used by the corporate bug tracking system,

thus it is critical that all managers and team members are existing users in LDAP. For

that reason, the controller automatically looks up the manager user name Bruce provides

in the corporate LDAP system to make sure that person exists. A team is only created if

the user name provided exists in the corporate LDAP server.


Bug List

       Each eNotify project has a set of bug lists. A bug list is a logical construct used to

define a group of bugs. Bugs for a given list are identified with a proprietary SQL-like

query syntax supported by the corporate bug data store. The specific details of the query

syntax cannot be shared as they are confidential, but it is important to state that this query

syntax is simply text from the perspective of the controller. The legacy generators use

these query strings to look up bug lists in the corporate bug database. In addition to the

bug list query, each bug list has a name, description, and the option to support

projections.

       In order for Bruce to complete the setup of his new project, he must add at least

one bug list to his project. The bug list page displays all of the bug lists for a given

project. Bruce accesses the bug list page by clicking on the “Bug Lists” column in the

server summary.




                                              39
       From the bug list page Bruce can view all of the settings for each bug list. As the

project’s administrator he also has the unique ability to create, edit or delete bug lists

from this page.




                  Figure 10 eNotify Web Administration Project Bug Lists

       The interface for adding bug lists is similar to that of adding teams. Bruce adds a

bug list by clicking on “Create New” in the top right of the bug list table. Once Bruce

has saved at least one team and one bug list his project can be generated. The controller

will assign the next available generator to generate his website.



Projections

       Projections are the number of bugs a team expects to have for each bug list at one

week intervals starting on the project’s projection start date and recurring for the number

of weeks identified by the project’s projection weeks setting. The manger of each team

Bruce identified when setting up the eNotify project has the unique ability to enter his

team’s projections in the eNotify controller. Projections are displayed in graphs as a tool

to measure a team’s actual bug count reduction against their planned bug count reduction.




                                              40
           Figure 11 eNotify Graph Showing Projection and Historical Bug Data




        While projections are only defined at the team level, a project’s projections are

the sum of all team projections for a given bug list.

        When Han, a team manager on Bruce’s project, wants to set his team’s

projections he goes to the eNotify web administration. Because projections are set in the

context of a specific project, team and bug list, the ability to edit projections is available

from the Team List page and the Bug List page for a given project. Thus Han can access

and edit his projections from two perspectives. He can visit the Team List page or the

Bug List page. When accessing projections in the context of the Team page, Han is

presented with the project’s bug lists. When accessing projections in the context of a Bug

page, Han is presented with the project’s teams.



                                              41
       Once both a team and a bug list have been chosen, Han is taken to a page

displaying a list of the projection dates and the bug counts that have been "projected" for

those dates.




               Figure 12 eNotify Web Administration View and Edit Projections

       If the logged in user is either one of the project’s administrators or the manager of

the team represented by the displayed projections, that user has the ability to edit the

projections by entering new bug counts for some or all of the projection dates. In the

above figure, Han is the team manager, so he is able to click on “Edit these projections”

to make changes to his team’s projected bug counts for the “Cavity Causing” bug list.




Online Help

       While the web administration layout is different from the legacy system’s, the

basic concept of projects that have teams and bug lists is consistent, and all of the project


                                             42
settings remain the same. The legacy online help was written to describe the details and

concepts behind the web administration settings and does not describe the web

administration layout. For that reason, the legacy online help required no change and was

simply included as a link in the new web administration.




                    Figure 13 eNotify Web Administration Online Help


Navigation

       Navigation is enabled by a hierarchical set of links at the top of every page. As

users navigate through a project’s pages a set of links appear at the top of the page that

provide both navigational context as well as a way to navigate back. The hierarchy

always starts with the list of all projects and becomes more specific as a project is

selected and the user navigates into specific details of the project.


                                              43
       Figure 14 eNoify Web Administration Navigation Hierarchy From Team Page


Authentication and Authorization

        Access to the web administration is protected basic HTTP authentication tied to

the corporate LDAP server.

        A project’s initial administrator is set to the current logged in user when the

project is created. That administrator has the option to add additional administrators to

the project by clicking on the administrator column on the project summary and using the

"Add Administrator" feature on the Project Administrators page. Once added as an

administrator to a project a user has the same capabilities as any other administrator on

the project. Every project must have at least one administrator. The web administration

allows administrators to remove themselves or other administrators from a project they

administer, as long as one other administrator remains associated with the project.

        Authenticated users have access to view all projects and project settings, but only

a project’s administrators have the ability to edit a project’s settings. Edit links are

available on any project where the current logged in user is an Administrator. Otherwise,

edit links are not visible.




                                              44
       For example in Figure 15, the user “Walter” is an administrator of the "Figment"

project, but not an administrator of any other project. Thus, when he is logged in the edit

link is only visible for the Figment project.




Figure 15 eNotify Web Administration: Authorization Restricted


         Definition of the Controller Schema and Migration of Legacy Data


       The ability to migrate legacy sites into the new controller is a mandatory

requirement of this solution. All features of the application hinge on the existence and

accessibility of the site configuration, thus creation of a working schema and a working

legacy to controller import script were the first priority.




                                                45
Schema Creation

        The controller schema was defined as part of the thesis proposal. Updates were

made to the original design to take advantage of Rails convention. Details of the changes

are described in Chapter 5.

        The central table in the schema is called projects. A project contains or relates to

the data needed to generate a legacy eNotify website. The key information needed by a

generator is the name of the site, its bug lists, and the list of team’s and owners associated

with the site.

        The concept of a person occurs in a number of places in eNotify. People can be

project administrators, team managers, and bug owners. People also play a role in many

projects. The logical construct of an owner provides a clear separation between a person

that may work on multiple projects, and the teams to which they belong.

        The ability for teams to store projections for a given bug list is enabled by the

projections table. Through the relationship of a project to its bug lists it is possible to

aggregate the projections of all teams for a given bug list to determine the cumulative

projections for that bug list at the project level. Thus, this schema provides a simple

mechanism for determining a critical piece of project information as an artifact of the

data provided by the teams.

        The controller uses the site update frequencies, generators and generations tables

as data points for determining when sites should be generated. This mechanism is

discussed in more detail later in this chapter and in chapter 5.




                                               46
Figure 16 Controller Schema



            47
       This schema was created using Rails migrations. While all legacy site data was

imported, some values in the database represented application level settings that were

stored in the database in order to allow for ease of enhancement in the future. Import

testing required frequent purging and recreation of the database so a mechanism was

required for easily repopulating application settings. A simple Rails mechanism was

used. Application data was manually populated into flat files in a human readable data

serialization format called YAML (YAML Ain’t Markup Language). Rails migrations

were updated to trigger the import of the YAML data into the database on schema

creation.


       An example of application data are the ownership models that are applied to bugs

in order to determine who owns the bug. Ownership models are effectively the name of

the algorithm that should be used to determine the owner of each bug in a bug list. During

the eNotify project setup the project manager assigns an ownership model to each bug

list. Examples of two frequently used ownership modes are the "State Based - default"

and “Owner is DE-Manager.”


       The "State Based - default" ownership model is the most commonly used by

project administrators. Its algorithm assigns bug ownership through a mapping of the bug

state and bug username fields. For each bug state, a unique username field is used to

identify the person that is considered the owner of the bug. For example, using the "State

Based - default" ownership model, a bug in the N (new) state is owned by the person

identified in the bugs 'DE-Manager' username field, because it is the responsibility of the

DE-Manager to review and assign the bug. The act of assigning the bug will change the

state of the bug and thus change the owner. When the state changes to A (assigned) it is


                                             48
the responsibility of an engineer to fix the bug. The "State Based - default" ownership

model identifies this person by assigning ownership to the person identified by the bug’s

'Engineer assigned' username field.


       The "Owner = DE-Manager" ownership model is used to determine ownership of

bugs that require the creation and review of customer facing documentation. The

manager associated with the bug owns this task. Thus, in this ownership model each bug

is owned by the person who's username appears in the 'DE-Manager' field of that bug.


       All ownership model algorithms were implemented as part of the legacy

application and were retained in their original form throughout the course of this project.

While the actual algorithmic logic for the ownership models is contained in the generator

code libraries, the controller needs a way to identify these algorithms such that a project

manager can choose the appropriate ownership model for a bug list, and the controller

can communicate that information to the generator.


       Ownership models are the secret sauce of eNotify, and while they have been

relatively static over the years, there have been a few instances where changing business

requirements created the need for a new ownership model. For example, the need to track

the ownership of the creation and review of customer-facing documentation for all bugs,

as described in chapter 1, was not originally provided by eNotify. An ownership

algorithm was added several years after its original release to support this business

requirement. While it is possible to hard-code ownership model values into the

controller, storing them in a table provides the ability to add new models without making

code changes to the controller in the future.



                                                49
       In order to populate ownership models into the controller database a YAML file

containing the hard coded ownership model values was created.


       DEFAULT:
         name: State Based - default
         legacy_name: DEFAULT
         description: see the online help

       STATE_BASED_MODEL_TWO:
         name: State Based - I state owner = Engineer then DE-Manager
         legacy_name: STATE_BASED_MODEL_TWO
         description: see the online help



[See appendix for the full file]



The following two lines were added to the ownership model Rails migration leading to

the automatic loading of this YAML data into the schema each time the migration was

run:


       directory = File.join(File.dirname(__FILE__),
                               "../constant_fixtures")
       Fixtures.create_fixtures(directory, "ownership_models")




Switching From a Perl to a Ruby Based Import Script

       During the proposal stage of this project I developed a prototype of the legacy

data import mechanism. From the thesis proposal document:

               The development and deployment of the system must to take into
               account the need to migrate sites from the existing system. The
               existing system is a critical component of several dozen ongoing
               projects and thus needs to be migrated without interrupting day to
               day operations. To that end, I’ve developed a proof of concept
               migration application. A prototype central controller web
               application consisting of a database schema and SOAP (World
               Wide Web Consortium, 2004) web services has been developed in
               Ruby on Rails. Via the web service it stores all the eNotify site


                                           50
               configuration data that is currently housed in flat files on the
               eNotify servers. A Perl script was developed that leverages the
               existing eNotify code base to collect a site’s configuration
               information and pass that information into the central controller
               web service. These prototypes demonstrate that it is possible to
               rapidly migrate the existing configuration data from the eNotify
               prototype to a central controller. This functionality will be refined
               as the new schema takes form and will be used in the final
               migration of sites to the new system.


       This prototype was based on the idea that there would be a greater gain in reusing

the existing generator code than there would be in lost time writing the database import

code in Perl. That assumption quickly proved itself false. The majority of the code I was

writing in Perl was to deal with database interactions such as saving an object and then

getting its database assigned ID to be used as a foreign key in another table.

       At the time I had not taken into account the simplicity with which the entire Ruby

on Rails stack would be generated during the schema creation. Ruby on Rails automates

much of the repetitive work through application creation scripts. Thus, the mechanism for

creating the migration files

       ruby script/generate scaffold ObjectModel



also created the model, controller, helper and view files. Once these pieces were in place

the model’s implementation of ActiveScaffold (Hansson, 2005) made database

interaction a trivial task. The code required to read the existing configuration files was

trivial to implement in Ruby using existing Ruby libraries for reading XML

(Rexml::Document) and CSV (Comma Separated Value) files.




                                             51
Mechanics of Importing Legacy Data

        The legacy application backs up all of its configuration and bug count history data

on a nightly basis. All of the data required to rebuild the site is in these backups. To

import a group of sites, the latest site backups are copied to a directory on the controller

named /usr/local/enotify/controller/sites_to_import which is scanned by the site importer

process.

        The site importer is implemented as an object that is instantiated by the script:

        ruby script/runner script/import/project.rb



        It reads through all of the backup sites in the sites_to_import directory and saves

the site configuration data to the database. The importer then calls methods on the project

object that are responsible for creating the directory structure on the NFS share where the

generators will place all generated files for this project. Finally the history files

containing the bug counts for each bug list for every previous generation of the legacy

site are copied from the backup directory to the NFS share. See the Graphing

Implementation section for more information on the use of history files.



                         Generator Communication Mechanics


        Communication between generators and the controller is set up as a request

response where the controller fields requests from generators. Generators are responsible

for initiating all communication, asking for a site to generate, and attempting to update

the controller with generation status. The controller is responsible for assigning work,

tracking progress, and error management.



                                               52
   The process for generating project websites is as follows:




1. A cron task launches the eNotify generator process.
2. The generator asks the controller which site to generate.
3. The controller responds with the name of a project.
4. The generator generates the site & sends status messages.
5. The generator finishes the site generation, notifies the controller and exits or
   requests a new site to generate.




             Figure 17 Generator and Controller Communication Flow




                                         53
       In order to keep the communication mechanism as simple as possible the

controller only supports two types of generator communications:


   1. Requests for a new project generation assignment
   2. Status updates for a generation in progress.


Status updates are simply messages saying the generator is starting or ending some part

of the site generation. For example:

GENERATION_BUG_LIST_DOWNLOAD_START means the generator is about to

start downloading the site’s bug lists from the corporate bug tracking system.

GENERATION_BUG_LIST_DOWNLOAD_END means the generator finished

downloading those bug lists.



There are six types of status updates

   1. GENERATION_START
   2. GENERATION_END
   3. GENERATION_BUG_LIST_DOWNLOAD_START
   4. GENERATION_BUG_LIST_DOWNLOAD_END
   5. GENERATION_RENDERING_PAGES_START
   6. GENERATION_RENDERING_PAGES_END



       The controller uses these status updates to record the progress of the generator in

the Generation object. Each status message corresponds to a column in the Generation

table. The controller records the current time in that column when it receives a status




                                             54
message. Generation status updates are used in determining the order of future project

generations.


Identifying Generators

       In order to simplify the deployment of new generators, the need to establish a

relationship between the controller and the generator is automatic. The controller

identifies each generator based on its IP address that is always passed as a request

parameter.

       It is technically possible for a generator using DHCP (IETF, 1997) to change its

IP address while generating a site, thus the enterprise deployment of this mechanism

requires generators with Static IP addresses.

       Communication security between the controller and generators is not a concern in

this deployment. The application is deployed behind a firewall and there is no

expectation of malicious tampering with the system. The communication APIs

functionality is limited to responding to generators requests for work and receiving

updates on that work. On the off chance that someone accidentally constructs a

completely valid POST request asking for a site to be generated, the worst case scenario

is the site does not generate. Enhancements to deal with this scenario are described in

the summary section.


Generator Communication Code

       Generator Controller communication was one of the few areas where new Perl

code was written for the generator in the form of a module called

eNotify::Controller::Communicator. The module encapsulates the mechanics of making


                                             55
REST based POSTS and UPDATES to the controller. Changes to the legacy generator

code are made extremely simple because it does not need any context for communicating

with the controller.

       Requesting a new site to update is accomplished with two simple lines of code.

       $controller_communicator =
       eNotify::Generator::ControllerCommunicator::new();
         $site_name = $controller_communicator->get_site_to_generate();



       Sending an update with the current status generation is a single line of code.

       $controller_communicator->update_generation_progress(
                                                 GENERATION_END);



       The controller uses the IP address discovered and passed by the

ControllerCommunicator module to determine which site the generator is generating. By

encapsulating the communication code in a separate module and implementing an

extremely simple communication protocol, the changes to the legacy generator were

limited to the addition and replacement of less than ten lines of code.




Error Management and Dealing With Rogue Generators

       Centralizing all error management in the controller eliminates the need to add

system logic to the generators and provides a single location to address any errors that

appear in the future. The term “Rogue generator” applies to any generator that does not

behave as expected. While a rogue generator producing errors due to a code bug is

unlikely given the stability of the legacy generators over the last 5 years, the error

management model still needs to take human error into account.


                                              56
       The most common symptom of a rogue generator is one that requests new work

before it has completed its previous generation assignment. Rebooting an eNotify

generator in the middle of a site generation easily reproduces this symptom.

       The results would appear as the following to the controller:


      A generator asks for a site to generate, does not notify the controller of generation

       completion but asks for another site to generate.


      A generator fails to report a status update while generating a site.


      A generator attempts to update the generation status for a site that it is not

       currently assigned to generate.

       In these cases, the controller has a minimum requirement: that a project

generation must have a recorded end time before it is considered finished. Other missing

status updates are ignored. If a generator requests a new site assignment without sending

a GENERATION_END message for its previous assignment, the controller responds

with the name of the existing assigned project. Attempts to send update messages by a

generator that is not currently assigned a site are logged and then ignored by the

controller.

       The mechanisms that deal with rogue generators trade off delays in generation for

work flow simplicity. It also eliminates the need to make changes to the legacy code.

While the goal is to reduce generation time, the likeliness of rogue generators is expected

to be so low that an investment in error checking and generation workflow speed was not

made. This is a topic of future enhancement in the summary section.




                                             57
                              Graphing Implementation


       eNotify graphs contain the historical bug counts for a bug list. Graphs are

rendered in the context of each owner, team and project.




          Figure 18 eNotify Graph Showing Projection and Historical Bug Data



Graphs display 4 pieces of information
   1. The date of each projection along the x-axis.
   2. The bug count for this bug list and the owner, team or project for all past
      projection dates (orange line).
   3. The projected bug counts for all projection dates (blue line).
   4. The current bug count (grey line and point).




                                            58
        The current bug count is displayed in gray on the upcoming projection date. It

updates each time the site generates and provides teams with a view of their progress

towards their next projection date milestone.




        Graphs are accessible from the generated eNotify pages by clicking on the graph

icon. The link to the graph contains the name of the bug list and the user name of the

team or owner. If the team and owner are excluded, the graph is generated for the entire

project. The project context is available through the bug list because each bug list belongs

to a specific project.



Graphing Implementation

        Graphs were implemented using Google Charts. Given the bug list and owner,

team or project context, three sets of data are required to populate the graph:


    1. The last generation date for the project.
    2. The projected bug counts.
    3. The historical bug counts.

        The last generation time and projected bug counts are provided as JavaScript

values by the controller. The projections contain both projection dates and bug counts and

are used to draw the x-axis labels as well as the projection bug count line.

        The historical bug counts are not available from the corporate bug database as

they are calculated in the context of the eNotify ownership models. The generator writes




                                             59
the historical bug counts to a JavaScript file, which is automatically imported via a

<SCRIPT> tag on the controller graph page.

        Projection dates do not always match one to one with stored historical data so

mapping between the data sets is done in real time. A simple JavaScript function on the

graph page iterates over each of the projection dates and finds a value in the historical

data that was recorded on, or one week before, the projection date. Using this method, a

history data point is drawn for every projection week that has passed.

        The final piece of information available on the graph is the current bug count. It is

calculated using the same method as is used to determined history for previous projection

dates, but it is drawn as a black data point on the upcoming projection date. Each time a

generator updates the site a new history record is made. This allows the current bug count

on the graph to reflect the exact value as the generated website pages.


Generator Changes to Support Graphing

        Two changes were required in the generator to support the migration of graphing

to the controller:


    1. Support for history data in JavaScript format
    2. Turning off the legacy graph generation.


        The first change added the JavaScript output format for history data. Legacy

generators store history data in CSV (Comma Separated Variable) flat files with each line

representing a data bug-count pair. In order to limit the changes in the generator, the CVS

file generation was left in place. This allows the generators to continue to consume and

use stored history without code changes.


                                             60
       A function was added to the generator to enable history data to be saved in a

JavaScript compatible format called JSON (JavaScript Object Notation). The generators

existing file save mechanism was updated to place history.js files containing JSON

formatted history data next to the existing history.csv files. A call to the JSON formatting

function was added next to the call to save the history file.




                                 Application Deployment


       Deployment of the application is key to its ability to meet the requirements of the

project. The controller and all generators are deployed on separate ESX Virtual Machines

running Red Hat Enterprise 4. This hardware configuration was chosen due to its

availability and ease with which generators can be cloned to add scale to the system. The

resulting deployment mirrors the proposed model in Figure 5.


                                    NFS Configuration


       An NFS share is set up containing the updated generator code, which contains the

repository for generated sites as a sub directory. The test rollout used a machine called

eNotify-gen-01 for this purpose. The controller and every generator mount this as a drive

by adding the following line to their local /etc/fstab

       eNotify-gen-01:/usr/local/eNotify/generator
       /usr/local/eNotify/generator nfs defaults 0 0




                                              61
Controller Configuration


        The controller is installed on a machine named eNotify-controller. MySql 5 is

installed on this machine as the database for the controller application. The controller

code is deployed in /usr/local/eNotify/controller. A directory,

/usr/local/eNotify/site_to_import, is created for importing legacy site data into the

controller. The import script copies legacy site data from the sites_to_import directory

into the controller.


        cd /usr/local/eNotify/controller
        ruby script/import/p


Supporting Generated Site URL Pass-Through

        In order to enable the controller to serve up generated site pages a symbolic link is

created on the controller machine.

        ln -s /usr/local/eNotify/generator/sites
        /usr/local/eNotify/controller/public/sites



        By providing access to the generated site pages through the controller/public

directory any site can be accessed using the controller’s base URL. For example a site

named Figment would be accessed via the URL

        http://eNotify-controller/sites/figment/index.html.




                                             62
Enabling Generators

       Generators access the generator code via their NFS mount of eNotify-gen-01 and

are activated with a simple cron job that runs the command

       Perl /usr/local/eNotify/generator/bin/eNotify.pl --gen --update –
       remote



       The --update and --gen --update parameters passed to the generators eNotify.pl

script are legacy parameters that tell the generator to download new versions of all the

bug list data and render the site pages.




                                            63
                 Chapter 5 Summary and Conclusions



       Since its inception, this thesis project generated considerable excitement from the

users of the legacy eNotify application. The promise of reduced operational costs, the

elimination of site server migrations, real time graphing, and the ability to leverage

existing resources to provide scale address all of the pain points in the legacy

application. With the completion of this project the company is poised to replace the

legacy system and realize these much needed improvements to one of its mission critical

applications. While the end users were not concerned with how these issues were solved,

there has been an ongoing concern about the feasibility of changing the existing legacy

application. The proposal and implementation in this project represented the first

solution that addressed both the legacy application issues and the practical limitations on

hardware and paid development resources.


                                   Comparison Metrics


       A key measure of the success of this project is the comparison of key metrics

pertaining to the capacity and use of the system. Comparing the new eNotify system with

the six existing legacy application installations, the following improvements are realized.




                                             64
Figure 19 Time to Complete Tasks - Legacy System vs Goals vs Results




   The maximum wait for graph re-rendering has been reduced from 24

    hours to ~1 second (the time it takes to refresh the graph web page).

   Excluding graph generation (described below) site generation duration

    remains unchanged, but the frequency of site updates, which was limited

    by a single server’s capacity in the legacy system, is now easily reduced

    with the simple addition of generators. The manual labor cost of adding a

    new generator has been reduced from 2 days to 1 hour.

   Turn around time for new project creation has been reduced from a 4 day

    wait for the end user, and a two hour task for the operations team, to a no-

    wait, instant, self-service experience for the end user with no effort

    required from the operations team.




                                  65
              The operations team spent 4 days per month completing the manual steps

               required for new site creation, changes to a site’s administrator, and

               launching a non-scheduled graph rendering. This cost was eliminated by

               the inclusion of self-service capabilities in the controller and instant graph

               generation.

              Load distribution achieved via manual site migrations across servers is

               eliminated. This saves the operations team an average of 9 person days of

               effort per migration.

       All of the requirements were addressed and the time and labor saving reductions

set out at the beginning of the project were met or exceeded. In addition to the planned

improvements there was one unexpected positive side effects of the chosen

implementation. The conversion from generation time rendering to dynamically

generated graphs eliminates 47 hours of graph rendering per day or just under 6 hours per

server, based on the existing legacy sites. This effectively adds two servers worth of

capacity to the new system given the existing server resources.


       Overall, the new eNotify system provides for more efficient use of hardware and

support personnel resources, while delivering an increased level of site generation service

and flexibility to the end users.




                                             66
                     Key Accomplishments and Lessons Learned


       Throughout this project a theme of finding the most practical tool for the job

emerged. Using Ruby to re-implement the web administration while reusing the existing

generators provided a practical mechanism for addressing the company’s needs, and put

the new support group in a position to enhance and customize the application with their

existing skill and resource set.

       At a high level this project was implemented exactly as it was proposed, but

during development the practicality theme reemerged at several points. The following are

five areas where the challenge of implementing the solution required overcoming

obstacles or changes to my original implementation plan. In each case choosing the right

tool or approach was a matter of practicality.




Convention Over Configuration with the Team Model

       The original controller schema was based on the legacy system. In it each project

has a set of teams and each team has a number of people on the team. The initial

controller model for this relationship was based on a single Owners Table. It contained

three columns:

       owners table:
       project_id: The ID of the project
       person_id: The ID of a person that was an owner on this project
       manager_id: The ID of the person that is person_id’s manager on
       this project



       In this model the logical construct of a team is created by looking up all of the

records in the Owners Table that have the project_id of the project in question and a



                                             67
common manager_id. The manager of the team is the person that has person_id ==

manager_id.

       As I was writing the code to deal with teams it quickly became clear that this was

a complex way to construct a team. It resulted in several hundred lines of code

responsible for creating, managing and manipulating teams. The simplicity of Rails

development favors convention over configuration, thus a large block of code dedicated

to managing team relationships was a signal that the convention I had chosen was not

optimal.

       The solution was to stick with the Rails modeling convention by creating an

actual team model and associating owners with that model.

       A team model was created with the following fields:

       team table:
       project_id: the project to which this team belongs
       person_id: the manager of this team



       The Owners Table was updated to have an owner belong to a team:

       new owners table:
       team_id: the team to which this owner belongs
       person_id: the id of a person on this team



       This change allowed me to replace those hundred lines of team code with

project.teams. I had selected a Rails development environment for the ease of

development and its mantra of convention over configuration, but it wasn’t until I

discovered and fixed my initial mistake of creating logical team relationships in the




                                            68
Owners Table that I fully appreciated the value of the Rails modeling paradigm which

makes it more obvious when a schema design mistake has been made.


ActiveScaffold - Record Level Policy & Permissions


       Half of the web administration was implemented using ActiveScaffold

(ActiveScaffold, 2009). ActiveScaffold is a powerful web administration generation

framework, which allowed for rapid development of GUIs related to projects and bug

lists. It is designed for rapid creation of system administrator GUIs and as a result has

limitations that impact its use in end user applications.

       The main challenge with ActiveScaffold in this project is its lack of support for

record level permissions. While ActiveScaffold has built-in security methods where

custom security logic can be implemented, it passes an empty copy of the current record

to those methods.

       In the case of this thesis project I had project objects (representing eNotify

websites) being passed to the security methods. The application required that I interrogate

the project and ask it if the current user was an administrator of the project. The

expectation was that the following project method would return true if the current logged

in user was a project administrator:

       def authorized_for_delete?

       administrators.each do |admin|
           if admin.username == current_user
               return true
           end
           end
           return false
       end




                                              69
       Surprisingly, this code always returns false because ActiveScaffold calls its

security methods as instance methods and as a result project.administrators is always

empty. This approach to security makes is such that the application could not provide

control over an end user’s permission to edit an individual project’s settings. In other

words, access to edit in ActiveScaffold was effectively all or nothing.

       One option for providing record level access is to alter the ActiveScaffold code.

Several suggestions detailing this approach are discussed on the ActiveScaffold forums.

In all cases, the solutions involve substantial changes to the ActiveScaffold code and

would require lengthy testing to determine if the changes were successful.



The Solution

       Given the application deployment environment there is no requirement for

hardened security and security by obscurity is considered sufficient by the management

team. The user base is behind a firewall and there is no expectation of internal users

attempting to hack the application. As such, it is sufficient to show and hide control of

user permissions based on the options they are shown on the administration web pages;

the link to edit project properties should show up if the logged in user is an administrator

for a site, and it should not show up if the user is not an administrator.

       I accomplished this with a simple addition to the project#show.html.erb

       <% if !@project.is_logged_in_project_administrator? %>
       <script>
       document.getElementById(‘projects-edit-<%= @project.id.to_s %>-
       link’).innerHTML = ‘‘;
       </script>
       <% end -%>




                                              70
       By default ActiveScaffold displays a link that allows any user to edit the project

properties. The above code adds JavaScript to the project page that removes the edit link

from the page if the logged in user is not the project administrator. This simple solution

leverages a standard cross browser JavaScript function that identifies links by their ID.

Setting innerHTML to an empty string deletes the link from the page.

       Using this pattern I was able to customize the ActiveScaffold output, to achieve a

practical level of record level permissions. While the solution does not stop someone

from manually constructing an edit link, it is completely sufficient for the user base of the

eNotify tool.


                 Identifying the Current Logged In User In the Model


       In order to make record level permissions available as described above, the

project model needed access to the user name of the currently logged in user.

       Since models do not have access to session variables, a mechanism was needed to

make the currently logged in user’s identity available in the project object. Rails does not

have a built in mechanism for accessing session variables in models because it assumes

session variables are only going to be accessed in the view and controller layers. In a pure

MVC application the model should not need to know about the session.

       The solution in this implementation is to attach the currently logged in user’s user

name to the thread associated with the incoming web request. In Mongrel, the default

web server shipped with Ruby on Rails, each web request is given its own thread. While

models do not have access to session data they can access the current thread, and thus are

able to get access to the user name of the current user.


                                             71
       There are limitations to this design pattern, the most problematic of which is

different threading models of different web servers. If this project is moved to another

web server this approach will need to be revisited. Even so, using request threads as a

mechanism for passing session data to models is an elegant way to address this issue.


Messaging Between Controllers and Generators


       Shortly after starting the project, the plan for communication between the

controller and generators was altered to use ActiveMQ to provide two-way guaranteed

delivery of messages between the processes. That decision turned out to be a mistake.

The two-way guaranteed delivery model enabled by ActiveMQ is very powerful.

Unfortunately, it results in a complex state machine where both the controller and the

generators needed to understand each other’s various possible states. Large amounts of

code were required on both the controller and the generator to support their various

states. Testing became increasingly complex and it wasn’t clear anything was gained with

all this complexity.

       The final straw was a limitation in the Ruby implementation of STOMP

(Streaming Text Orientated Messaging Protocol) . The package does not currently

support the concept of publishing to a temporary queue. A goal of the controller -

generator communication was to allow the controller to assign tasks to specific

generators. The way to address them was to use a temporary queue whose name was the

name of the generator. While there was a work around for publishing to temporary

queues the solution itself was complicated and required synchronization of large blocks




                                            72
of code to avoid threading bugs. The combination of complex messaging with the threat

of threading bugs caused me to drop ActiveMQ.

       The solution was once again very simple. The communication mechanism was

switched to two simple web services provided by the controller to the generators. It

supported two types of calls from the generators:


   1. Get a site to update.
   2. Update the status of a site generation.



       Generators are responsible for asking for work and attempting to update the

controller with generation status. The controller is responsible for assigning work and

tracking progress. If a generator asks for a new site to update before it reports finishing

its previous assignment (most likely due to a generator server reboot) the controller

simply gives the generator its previous assignment again.

       With this communication model the generators are able to behave like drones with

no responsibility for state management while the controller has a limited set of states to

manage.


Addressing the Moving Projections Target

        A major challenge in identifying projection values and historical bug counts for

projection dates is dealing with user changes to the projection dates. A projection is the

number of bugs that a team expects to have for a given bug list on a given date.



Projection dates are determined by 2 factors:



                                             73
   1. The Projection Start Date: (Set in the controller DB as a project property).
   2. Projection Weeks: The number of weeks from the projection start date that
      projections should be measured.

       To support this concept the projection table has the following fields:

       projections table:
       project_id
       team_id
       date
       bug_count

       A team manager for a specific bug list records projections on a specific date. For

example given:


       projection Start Date = 3/2/2009
       projection weeks = 4

       The dates for which a team needs to set projections are:


       3/2/2009, 3/9/2009, 3/16/2009, 3/23/2009


       The challenge comes if an administrator changes the projection start date. Even

though existing team projections no longer map directly to a project projection date, the

projections the teams entered still have meaning.

       To be able to use projections that don’t match the exact project projection weeks,

an algorithm was implemented that matches the project projection weeks to the team

projection that has a date less than or equal to 1 week before the project projection date.

This allowed for the flexibility to move projection dates without losing existing

projection settings.




                                             74
                                 Future Plans for eNoitfy


        The change to a Rails based administration has greatly simplified the work

required for future development. The following are some examples of feature

enhancements that have been suggested to the operations team that will take ownership of

the project.


Disabling a Site From Generating

        Even in the new system load is an issue. Adding generators to support additional

sites is relatively inexpensive, but there is no desire to be wasteful. Sites that do not need

updates should be automatically disabled. It is possible to identify these sites based on

their project settings.

        If a project’s projection end date (project.projection_start_date *

project.projection_weeks) has passed then there is no reason to render the site. In the

legacy system this would have required changes to multiple files and the deployment of

the code to multiple servers. With the switch to Ruby on Rails this change is now a one-

line change to the project model described by app/models/project.rb.


Predicting the Next Generation Time For a Site

        A common question asked by administrators is "When will my site update next?"

It was not possible to answer this question in the legacy system, but the data stored by the

controller provides the possibility for a next generation time heuristic calculation.



                                              75
       For example, by taking the last 10 project.generations and averaging their

duration it is possible to make a reasonable guess as to how long a site will take to

update. It is also possible to make an ordered list of which sites will update by extending

the logic used to determine the next site to update. By combining these two pieces of data

it is possible to add the expected rendering duration of all the sites that are scheduled to

update before another site. Doing so provides a reasonable estimation of the next

generation time for a given site.


Partitioning eNotify Scale Based On Organizations

       This project enables any user to create eNotify sites at will. The application is

used across many organizations while being supported by the hardware resources of only

one organization. As it is implemented there is no feedback mechanism between the

people creating projects and adding load, and the people responsible for scaling the

system. This has the potential to lead to unintentional abuse of system resources by the

people creating new projects.

       An enhancement could be made to the system to associate specific projects with a

specific pool of generators. This would create a negative feedback loop for groups who

abuse the system by adding many sites without disabling other sites. As their generators

get overloaded their sites will refresh less frequently. They have the choice to disable

unused but still active sites, or request additional generators, but maintaining

performance levels requires a conscious effort and thus the system is less likely to be

abused.




                                              76
       This can be implemented by adding a single field to projects,

project.business_unit, and a similar field to generators, generator.business_unit and then

making a one line change to scheduler, scheduler.rb#schedule_next which contains the

code responsible for assigning work to generators.


Augmenting security

       The current implementation of web administration security as described in chapter

5 under the section “ActiveScaffold - Record Level Policy & Permissions.” This

approach is effectively security through obscurity using client side JavaScript to hide

functionality from users that should not see that functionality. While it is sufficient for

this initial release it could be exploited in the future as the business and culture of eNotify

use evolves.

       An enhancement could be made to the system that would move the logic for

security into the server rather than the client and provide a more secure implementation.

This can be accomplished by removing ActiveScaffold from the project and recreating

the pages it renders using the Ruby on Rails standard embedded Ruby (.erb) format.




Summary of System Update Capability


       These enhancements could have been implemented as part of this project, but it is

not clear that their functionality is desired. What is important and beneficial to the end

user is the previously unavailable ability for the operations team to rapidly evolve the

application to optimize for changing business needs.




                                              77
                                      Glossary



       The following list describes the terms and abbreviations used in this document.




ActiveScaffold: A Ruby on Rails plug-in that provides a rich featured out of the box web

user interface experience intended for use with web administration applications.


Central Controller: Component of the solution responsible for providing the

administration user interface, site data storage, business logic control and programmatic

access to configuration data.


CSV: An abbreviation for Comma Separated Value. CSV files are used to store data in

comma-separated lists.


eNotify: The branded name for the application being replaced by this project.


GD::Graph: A Perl module used to render graphs in the legacy application.


HTML: Hyper Text Markup Language


JSON: JavaScript Object Notation


LDAP: Lightweight Directory Access Protocol


Mongrel: A web server shipped with Ruby on Rails




                                            78
MySql: An open source relational database management system


NFS: Network File System


Project Web Site: The collection of web pages that are generated for a single eNotify

bug-tracking project.


QDDTS: A web based bug query tool that returns lists of bugs based on a supplied query

string. The entomology of this name is unknown.


Rexml:Document: A Ruby library used for parsing and creating XML documents.


Site: Term describing the collection of configuration data and web pages generated for a

project by the eNotify system. Each company project that uses eNotify will have a site for

their project.


STOMP: Streaming Text Oriented Messaging Protocol.


UI: User Interface


VMware ESX: Virtual Machine Enterprise Server is a commercial virtual machine

server capable of hosting up to 80 resident virtual Machines.


YMAL: “YAML Ain’t Markup Language” is a recursive acronym for a human readable

data serialization format frequently used with Ruby on Rails.




                                            79
                                    References




ActiveScaffold. (2009). ActiveScaffold - A Ruby on Rails plugin from the makers of
    AjaxScaffold. http://activescaffold.com/. retrieved March 29, 2009.
Aptana, Inc. (2006). RadRails Services. http://www.radrails.org/ retrieved March 3, 2007.
Burbeck, S. (1992). Applications Programming in Smalltalk-80(TM): How to use Model-
    View-Controller (MVC). http://www.cs.uiuc.edu/users/smarch/st-docs/mvc.html.
    retrieved March 3, 2007.
Eclipse Foundation (2007). Eclipse - an open development platform.
    http://www.eclipse.org/. retrieved March 3, 2007.
Hansson, D. H. (2005). Ruby on Rails. http://www.rubyonrails.org/. retrieved March 3,
   2007.
Hass H. (2005). Reconciling Web Services and REST Services.
    http://www.w3.org/2005/Talks/1115-hh-k-ecows/#(1). retrieved March 3, 2007.
Hibernate (2009). Hibernate Shards – Horizontal Partitioning With Hibernate.
    http://www.hibernate.org/hib_docs/shards/reference/en/html/. retrieved March 29,
    2009.
IETF. (1997). Dynamic Host Configuration Protocol. http://tools.ietf.org/html/rfc2131.
   retrieved March 29, 2009.
IETF. (2006). Lightweight Directory Access Protocol (LDAP): Technical Specification
   Road Map. http://tools.ietf.org/html/rfc4510. retrieved March 29, 2009.
MacroMates. (2009). TextMate – The Missing Editor for Mac OS X.
   http://macromates.com/. retrieved March 28, 2009.
Morimoto, R., Noel, M., Droubi, O., Mistry, R., &Amaris, C. (2008). Windows Server
   2008 Unleashed, United States of America: Sam Publishing.
Perl Foundation. (2002). Perl. http://www.perl.org/. retrieved March 3, 2007.
Pestov, S. (2004). jEdit: Programmer’s Text Editor. http://www.jedit.org/. retrieved
    March 3, 2007.




                                            80
Red Hat, Inc. (2005). Network File System. In Red Hat Linux 9: Red Hat Linux
    Reference Guide. http://www.redhat.com/docs/manuals/linux/RHL-9-Manual/ref-
    guide/ch-nfs.html. retrieved March 3, 2007.
Red Hat Middleware. (2006). Hibernate: Relational Persistence for Java and .NET.
    http://www.hibernate.org/. retrieved March 3, 2007.
Stevens. (2002). Advanced Programming in the UNIX Environment, Boston: Addison-
    Wesley.
Sun Microsystems. (2002). Model View Controller.
    http://java.sun.com/blueprints/patterns/MVC.html. retrieved March 3, 2007.
Sun Microsystems. (2008). My SQL :: The world's most popular open source database.
    http://www.mysql.com/. retrieved March 28, 2009.
Thomas, D. (2005). Programming Ruby. Dallas: The Pragmatic Bookshelf.
Verbruggen, M. (1999). GD::Graph. http://search.cpan.org/~mverb/GDGraph-
    1.43/Graph.pm. retrieved March 3, 2007.
VMware, Inc. (2007) VmWare. http://www.vmware.com/products/vi/esx/ retrieved
  March 3, 2007.
Wardley A. (2007). Template Toolkit 2. http://www.template-toolkit.org/. retrieved
   March 3, 2007.
Waterman R. (1999). Mod CGI.
   http://gosset.wharton.upenn.edu/teaching/540/class_cgi/Notes/node4.html. retrieved
   March 3, 2007.
World Wide Web Consortium. (2007). XHTML2 Working Group Home Page.
   http://www.w3.org/MarkuUp/. retrieved April 2007.
World Wide Web Consortium. (2007). Web Services. http://www.w3.org/2002/ws/.
   retrieved March 3, 2007.
World Wide Web Consortium. (2004). Simple Object Access Protocol (SOAP).
   http://www.w3.org/TR/soap/. retrieved March 3, 2007.
World Wide Web Consortium. (1999). Common Gateway Interface (CGI).
   http://www.w3.org/CGI/. retrieved March 3, 2007.
Zawodny, J. (2009). Overcoming MySQL's 4GB Limit.
   http://jeremy.zawodny.com/blog/archives/000796.html. retrieved March 28, 2009.




                                           81
                     Appendix 1 Application Code



      The source code was maintained in Google Code at

      http://code.google.com/p/thesisdev/source/checkout.


                          Cascading Style Sheet (.css)



public/stylesheets/custom.css

.message {

    width:700px;
    background-color:#fcfbc0;
    padding:15px;



}

.label_title {

    font-size: 130%;
    font-weight: bold;


}




                                         82
                             Embedded Ruby (.erb)



app/views/administrators/edit.html.erb
<h1>Editing administrator</h1>

<%= error_messages_for :administrator %>

<% form_for(@administrator) do |f| %>
  <p>
    <b>Project</b><br />
    <%= f.text_field :project_id %>
  </p>

  <p>
    <b>Person</b><br />
    <%= f.text_field :person_id %>
  </p>

  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<%= link_to 'Show', @administrator %> |
<%= link_to 'Back', administrators_path %>




                                         83
app/views/administrators/for_project.html.erb
<%= render :active_scaffold => 'administrators', :constraints =>
{:project_id => @project.id} -%>


<script>


<% if !@project.is_logged_in_project_administrator? %>
        <% @project.administrators.each do |admin| %>
                document.getElementById('administrators-destroy-<%=
admin.id.to_s %>-link').innerHTML = '';
        <% end -%>
<% else -%>
document.getElementById('mod_actions').innerHTML = '<a
href="/administrators/new/<%= @project.id.to_s %>" class="new action"
id="administrators-new--link" position="top">Add a new
Administrator!</a>' + document.getElementById('mod_actions').innerHTML;
<% end -%>


</script>




                                       84
app/views/administrators/index.html.erb
<h1>Listing administrators</h1>

<table>
  <tr>
    <th>Project</th>
    <th>Person</th>
  </tr>

<% for administrator in @administrators %>
  <tr>
    <td><%=h administrator.project_id %></td>
    <td><%=h administrator.person_id %></td>
    <td><%= link_to 'Show', administrator %></td>
    <td><%= link_to 'Edit',
edit_administrator_path(project_administrator) %></td>
    <td><%= link_to 'Destroy', administrator, :confirm => 'Are you
sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New project_administrator', new_administrator_path %>




                                          85
app/views/administrators/new.html.erb
<%= error_messages_for :administrator %>

<div class="message">
Administrators have complete control over an enotify site. They can
change any project setting, and or update bug lists, ad or update teams
and even add other administrators. So make sure you like this person
before you add them as an admin to your enotify site.

<br><br>

To add a new Administrator to the <%= @project.name %> project enter
the new administrator's username and
click 'Add this very trusted friend as an administrator'.

</div>


<% form_for :administrator, :url => { :action => :create } do |form| %>


<p>Administrator Username: <%= form.text_field 'username' %> </p>
<%= form.hidden_field('project_id', {:value => @project.id}) %>

<BR><BR>
<%= form.submit "Add this very trusted friend as an administrator" %>

<%= link_to("Never mind", :controller => "administrators", :action =>
"for_project", :id => @project.id) %>


<% end %>




                                        86
app/views/administrators/show.html.erb
<%= render :active_scaffold => 'administrators', :constraints => {:id
=> params[:id]} -%>




                                         87
app/views/bug_lists/for_project.html.erb
<%= render :active_scaffold => 'bug_lists', :constraints =>
{:project_id => @project.id} -%>


<script>

<% if !@project.is_logged_in_project_administrator? %>
               document.getElementById('bug_lists-new--link').innerHTML
= '';
               document.getElementById('bug_lists-new--
link').style.visibility = 'hidden';

        <% @project.bug_lists.each do |bug_list| %>
                document.getElementById('bug_lists-destroy-<%=
bug_list.id.to_s %>-link').innerHTML = '';
                document.getElementById('bug_lists-edit-<%=
bug_list.id.to_s %>-link').innerHTML = '';
        <% end -%>
<% else -%>
<% end -%>
</script>




                                           88
app/views/graphs/show.html.erb
<HTML>
<HEAD>
        <meta http-equiv="Pragma" content="no-cache">
        <meta http-equiv="Expires" content="-1">
        <script src="/enotify-resources/js/tools.js"></script>
    <LINK REL="stylesheet" TYPE="text/css" HREF="/enotify-
resources/css/tools_styles.css">
        <TITLE>Harvard eNotify v8</TITLE>

<% if @team.nil? %>
        <script src=<%= @project.site_history_url + @bug_list.name +
'.js'%> ></script>
        <% else %>
        <script src=<%= @project.site_history_url +
@team.manager.username + '/' + @bug_list.name + '.js'%> ></script>
        <% end -%>



<script>

TEST_MODE = false;

/* this is a logging function that works with mozilla firebug */
function plog(content)
{
    if (TEST_MODE)
    {
        console.log(content);
    }
}


    var table_title = 'foo';

    var summary_link = 'bar';

        // get the projection javascript
        <% if @team.nil? && @owner.nil? %>
                // these are the projections for the entire project
                var projection_data = <%=
Projection.as_js_array(Projection.find_projections_by_bug_list(@bug_lis
t)) %>;
        table_title = "<%= @project.name %> - <%=
@bug_list.display_name %> - as of <%=
@project.latest_complete_generation_time.to_s %>";
        summary_link = '<a href="<%= @project.site_website_url
%>index.html">Project Summary</a>';
    <% elsif !@owner.nil? %>
        // this is the project page
        table_title = "<%= @owner.full_name %> - <%=
@bug_list.display_name %> - as of <%=
@project.latest_complete_generation_time.to_s %>";



                                   89
        summary_link = '<a href="<%= @project.site_website_url +
@owner.username %>/individual_summary.html">Owner Summary</a>';
        <% else %>
                // these are the projections for on team
            var projection_data = <%=
Projection.as_js_array(Projection.find_projections_by_team_and_bug_list
(@team, @bug_list)) %>;
        table_title = "<%= @team.manager.full_name %>'s team - <%=
@bug_list.display_name %> - as of <%=
@project.latest_complete_generation_time.to_s %>";
        summary_link = '<a href="<%= @project.site_website_url +
@team.manager.username %>-team/username_team_summary.html">Team
Summary</a>';
        <% end -%>

        // The last time the history generated for this site.
        <% if @project.latest_complete_generation_time %>
                var current_date = <%=
@project.latest_complete_generation_time.to_i.to_s %>;
        <% else %>
                var current_date = 0;
        <% end -%>

       // this is for testing purposes only
       //current_date = 1182488900;

    var ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7;


    var history_array = new Array();
    var proj_array = new Array();
    var current_array = new Array();

    var max_value = 25;

    var last_projection_index_with_absolute_history = 0;

    // define the properties for the graph
    var chart_props = new Array(
    'http://chart.apis.google.com/chart?chs=320x200',
    'cht=lxy',
    'chs=700x375',
    'chls=2,1,0|2,5,3|2,1,1',
    'chco=FF6600,0000FF,000000',
    'chdl=Actual|Projections|Current',
    'chm=N,000000,1,-1,8,1|N,000000,0,-
1,12,1|o,FF6600,0,1,10,0|N,000000,2,-1,12,1|o,CCCCCC,2,-
1,6,0|o,0000FF,1,-1,8,0',
    'chdlp=b',
    'chxt=x',
    'chxl=0:' + calulate_x_axis_label() );


    // build the URL for the chard
    function get_url()
    {



                                     90
      return chart_props.join("&") + "&" + chart_max_height() + "&" +
chart_data();
    }

        // build a string containing the chart data
    function chart_data()
    {
        return 'chd=t:-1|' + history_array.join(',') + '|-1|' +
proj_array.join(',') + '|-1|' + current_array.join(',');
    }

    function chart_max_height()
    {
        return 'chds=0,' + get_max_value();
    }

    function render_url()
    {
        return '<img src="' + get_url() + '">';
    }

    function set_max_value(possible_max)
    {
        if ( max_value < parseInt(possible_max) )
        {
            max_value = parseInt(possible_max);
        }
    }

    function get_max_value()
    {
        return max_value + max_value * 0.1;
    }

    // finds the max projection data and puts
        // the projections in a sting for the chart url
    function calculate_projection_data()
    {
        for (var j = 0; j < projection_data.length; j++)
        {
            set_max_value( projection_data[j][1]);
            proj_array.push( projection_data[j][1]);
        }
        return proj_array.join(',');
    }


    // for every date in the projection data find a matching history
entry
    // if there is no entry, put -1 so nothing is drawn on the graph
    function calculate_history_data()
    {
        plog("calculate_history_data()");
        plog("projection_data.length " + projection_data.length);
        plog("current_date " + current_date);
        for (var j = 0; j < projection_data.length; j++)
        {


                                   91
                  // you might have history after the current date,
                  // but we don't want to show that on the history line
                  // because it's not fixed yet. We'll handled the current
count
            // on the current count line.
            if ( projection_data[j][0] <= current_date )
            {
                plog("checking projection date " +
projection_data[j][0]);
                var history_for_date =
get_history_value_for_date(projection_data[j][0], history_data);

                         if ( history_for_date != '-1' )
                         {
                             // this is the last projection index for which
there is an
                             // history entry
                             last_projection_index_with_absolute_history = j;
                         }
                         history_array.push(history_for_date);
                  }
                  else
                  {
                         history_array.push(-1);
                  }

            }

    }



    // given a date and the history data, this function will
    // determine the bug count for that date
    function get_history_value_for_date(requested_date, history_data)
    {

            plog( "Requested Date: " + requested_date) ;
            var the_answer = '-1';

            for       (var i = 0; i < history_data.length; i++)
            {
           if ( history_data[i][0] <= requested_date &&
                history_data[i][0] > (requested_date -
ONE_WEEK_IN_SECONDS )   )
           {
                set_max_value(history_data[i][1]);
                plog("hello world")
                // the count found is the bug count on or before
                the_answer = history_data[i][1];
                // don't do any more checks for this date
                return history_data[i][1];

               }
            }
            return the_answer;
        }


                                            92
       // create a data set that shows the current bug count as a line
from
           // the last absolute bug count projected out to the next week
at its
           // current value
       function calculate_current_data()
       {


           for       (var j = 0; j < projection_data.length; j++)
           {
                 if ( j < last_projection_index_with_absolute_history )
                 {
                     // don't show anything for the day's we have history
                     current_array.push(-1);
                 }
                 else if ( j == last_projection_index_with_absolute_history
)
                 {
                current_array.push(history_array[j]);
            }
            else if ( j - 1 ==
last_projection_index_with_absolute_history )
            {

current_array.push(get_history_value_for_date(projection_data[j][0],
history_data));
            }
            else
            {
                 current_array.push(-1);
            }


           }


       }

       // formats the x axis label based on the
           // number of weeks in the graph
       function calulate_x_axis_label()
       {
          //it has to look like this
          //'chxl=0:|jan|feb|mar|apr|may|jun'
          var chxl = '';
          for (var j = 0; j < projection_data.length; j++)
          {
              var d = new Date( projection_data[j][0] *1000 );
              var curr_date = d.getDate();
              var curr_month = d.getMonth();
              curr_month++;
              chxl += '|' + curr_month + '/' + curr_date;
          }


                                           93
           return chxl;

     }
     calculate_projection_data();
     calculate_history_data();
     calculate_current_data();




            /*
                  The following is UI rendering code from the legacy
website pages
                  Since the goal is to make this page look like the legacy
system
                  it is being reused here.
             */
         tblFactory.width=700;

         tblFactory.title = table_title;
         tblFactory.help = "understanding_graphs.htm";
             // get the title for this bug list


           tblFactory.link=summary_link;


         var tbl = tblFactory.createTable();

         tblFactory.reset();
         colFactory.dividerStyle=ALTERNATING;
         var spacer=colFactory.createColumn();
         spacer.add(renderLiner(1,15));


         var tblCol1=colFactory.createColumn();


         colFactory.reset();

         tblCol1.add("<IMG SRC='" + get_url() + "'>");


         //tbl.add(spacer);
         tbl.add(tblCol1);
         //tbl.add(spacer);




    //     PAGE LAYOUT

         var pl = new PageLayout(800,1,0);


                                      94
         pl.add(tbl.render());

</SCRIPT>




<head>


<body topmargin="0" leftmargin="0" alink="Blue" bgcolor="#ffffff"
vlink="Blue" link="Blue" marginheight="0" marginwidth="0">
<table border="0" cellpadding="5" cellspacing="0" width="800"
height="28">
    <tbody>
    <tr>
         <td align="left" bgcolor="#000000">
             <font color="WHITE" size="-1" face="arial"><b> </b></font>
         </td>
         <td align="right" bgcolor="#000000">
             <a href="http://flubber-
tools.harvard.com/tools/projects/enotify/whatsnew" target="_blank">
             <font color="yellow" size="-1"
face="arial"><b>New Features</b></font></a>
             <font color="black">—</font><a href="http://flubber-
tools.harvard.com/tools/projects/enotify" target="_blank">
             <font color="white" size="-1"
face="arial"><b>Home</b></font></a><font color="black">—</font><font
color="black">—
             </font><font color="white" size="-1" face="arial">Updated:
<%= @project.latest_complete_generation_time.to_s %></font>
         </td>
    </tr>
    </tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="800"
height="28">
    <tbody>
    <tr>
    <td> </td>
    <td>
         <table border="0" cellpadding="0" cellspacing="0" height="28">
             <tbody>
             <tr>
             <td nowrap="nowrap"><font face="arial"><b><a href="<%=
@project.site_website_url %>index.html">eNotify - <%= @project.name
%></a><b></b></b></font></td></tr><tr><td nowrap="nowrap">
             <font size="+3" face="verdana">Trend
Graph</font></td></tr></tbody></table></td><td align="right"
nowrap="nowrap">
             <img src="/enotify-resources/images/logo.gif" alt=""
border="0"></td><td> </td></tr></tbody></table><br>


                                   95
            <div style="margin-left:3em;">
<script>
 document.writeln(pl.render());
</script>

<% if !@team.nil? %>
<%= link_to( "Set Projections for this graph", :controller =>
"projections", :id => @project.id, :team_id => @team.id, :bug_list_id
=> @bug_list.id, :action => :show)%>
<% end -%>

<div style="display:none;">

<h1>show graphs </h1>
<p>Project: <%=h @project.name%>
        <% if @team.nil? %>
        ( no team )
        <% else %>
        -- <%= @team.manager.full_name %>'s team
        <% end -%>

        </p>
<p>Bug List: <%=h @bug_list.display_name %></p>

<!-- this history.js will need to be replaced with @project/team get
history url get_history -->
<p>     HISORY SCRIPT <% if @team.nil? %>
         src=<%= @project.site_history_url + @bug_list.name + '.js'%>
        <% else %>
         src=<%= @project.site_history_url + @team.manager.username +
'/' + @bug_list.name + '.js'%>
        <% end -%>
</p>
<br><br>
<script>document.writeln("max valueL " + max_value);</script>
<br>
<script>document.writeln("projections: " +
proj_array.join(','));</script>
<br>
<script>document.writeln("history: " +
history_array.join(','));</script>
<br>
<script>document.writeln("current: " +
current_array.join(','));</script>
<br>
<script>document.writeln("x axis: " +
calulate_x_axis_label());</script>
<br>

<script>document.writeln(history_data.join(','));</script>

</div></div>




                                   96
app/views/layouts/admin_standard.html.erb
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>E-Notify Administration</title>
  <%= stylesheet_link_tag 'scaffold' %>
  <%= stylesheet_link_tag 'custom' %>
  <%= javascript_include_tag :defaults %>
  <%= active_scaffold_includes %>
</head>
<body>

        <div style="float:left;">
                <b><a href="/projects" style="text-
decoration:none;color:#000000;">E-Notify Administration</a></b>
                <a href="http://i">Support</a>
                <a href="/enotify-
resources/online_documentation/documentation/eNotify.htm"
target="_blank">Online Help</a>
        </div>
        <div style="float:right;">
                <b><%= session[:username] +'@' + EMAIL_DOMAIN %></b> |
                <%= link_to "Settings", :controller => :people,
:username => session[:username], :action => :show %> |
                <%= link_to "Logout", :controller => :login, :action =>
:logout %>
        </div>
<div id="header_bar" style="clear:both;border-bottom:solid #CCCCCC
1px;padding-top:5px;"></div>

<h2><%= link_to 'Projects', :action => 'index', :controller =>
'projects' %>
<% if !@project.nil? %>
  >> <%= link_to @project.name, :action => 'show', :controller =>
'projects', :id => @project.id %>
<%end%>
<% if params[:controller] == 'teams' && params[:action] ==
'for_project' %>
  >> Teams
<%end%>
<% if params[:controller] == 'teams' && params[:action] == 'new' %>

        >> New Team
<% end -%>

<% if (params[:controller] == 'teams' || params[:controller] ==
'owners') && !@team.nil? && !@team.project.nil? %>
               >> <%= link_to("Teams", :controller => "teams", :action
=> "for_project",
                                                     :id =>
@team.project) %>



                                     97
    <% if params[:controller] == 'teams' %>
                 >> <%= @team.manager.full_name %>'s team

        <% end %>
        <% if params[:controller] == 'owners' %>
                >> <%= link_to( @team.manager.full_name + "'s team",
:action => "show", :id=> @team.id) %>
        <% end %>
<%end%>

<% if params[:associations] == 'bug_lists' || params[:controller] ==
'bug_lists' %>    >> Bug Lists <%end%>
<% if params[:controller] == 'administrators' %>
        <% if params[:action] == 'for_project' %>
                >> Administrators
        <% end -%>
        <% if params[:action] == 'new' %>
                >> <%= link_to( "Administrators", :action =>
"for_project", :id=> @project.id) %>
                >> Add New
        <% end -%>
<%end%>

<% if params[:controller] == 'projections' && params[:action] ==
'index_for_bug_list' %>
          >> <%= link_to @bug_list.project.name, :action => 'show',
:controller => 'projects', :id => @bug_list.project.id %>
        >> <%= link_to "Bug Lists", :action => 'for_project',
:controller => 'bug_lists', :id => @bug_list.project.id %>
        >> <%=h @bug_list.display_name %>
<% end -%>


<% if params[:controller] == 'projections' && params[:action] ==
'index_for_team' %>
          >> <%= link_to @team.project.name, :action => 'show',
:controller => 'projects', :id => @team.project.id %>
        >> <%= link_to "Teams", :action => 'for_project', :controller
=> 'teams', :id => @team.project.id %>
        >> <%= link_to @team.manager.full_name + "'s team", :action =>
'show', :controller => 'teams', :id => @team.id %>

<% end -%>

<% if params[:controller] == 'projections' && params[:action] == 'show'
%>
          >> <%= link_to @team.project.name, :action => 'show',
:controller => 'projects', :id => @team.project.id %>
        >> <%= link_to "Teams", :action => 'for_project', :controller
=> 'teams', :id => @team.project.id %>
        >> <%= link_to @team.manager.full_name + "'s team", :action =>
'show', :controller => 'teams', :id => @team.id %>
<% end -%>




                                   98
<% if params[:controller] == 'projections' && params[:action] ==
'edit_group' %>
          >> <%= link_to @team.project.name, :action => 'show',
:controller => 'projects', :id => @team.project.id %>
        >> <%= link_to "Teams", :action => 'for_project', :controller
=> 'teams', :id => @team.project.id %>
        >> <%= link_to @team.manager.full_name + "'s team", :action =>
'show', :controller => 'teams', :id => @team.id %>
<% end -%>

<% if params[:controller] == 'generations' %>
          >> <%= link_to @generation.project.name, :action => 'show',
:controller => 'projects', :id => @generation.project.id %>
      >> Generation
<% end -%>


</h2>
<p style="color: green"><%= flash[:notice] %></p>
<p style="color: yellow"><%= flash[:warning] %></p>
<%= yield %>

</body>
</html>




                                   99
app/views/login/index.html.erb


<% form_for :login, :url => { :action => :login } do |form| %>


<p>Username: <%= form.text_field 'username' %> </p>
<p>Password: <%= form.text_field 'password' %> </p>

<%= form.submit "Login" %>

<% end %>




                                   100
app/views/owners/new.html.erb

<%= error_messages_for :owner %>
<div class="message"><span class="label_title">Add a Team
Member</span><br><br>
  To add a new owner to <%= @team.manager.full_name %>'s team enter the
username of that
  person as it would appear in a DDTS bug field and click 'Add this
owner'.
</div>
<br><br>


<% form_for :owner, :url => { :action => :create } do |form| %>


<p>Team Lead Username: <%= form.text_field 'username' %> </p>
<%= form.hidden_field('team_id', {:value => @team.id}) %>

<BR><BR>
<%= form.submit "Add new team member" %>

<%= link_to("Never mind", :controller => "teams", :action => "show",
:id => @team.id) %>


<% end %>




                                   101
app/views/people/edit.html.erb
<div class="message">Hi <%=h @person.full_name %>, <br>This is your big
change to reinvent yourself.
The name you enter below will be your name on all future enotify
reports.
</div>

<%= error_messages_for :person %>

<% form_for(@person) do |f| %>

  <p>
    <b>First name</b><br />
    <%= f.text_field :first_name %>
  </p>

  <p>
    <b>Last name</b><br />
    <%= f.text_field :last_name %>
  </p>

  <p>
    <%= f.submit "Update" %>      <%= link_to 'Never Mind', :controller
=> :people, :username => @person.username, :action => :show %>
  </p>
<% end %>




                                      102
app/views/people/show.html.erb
<div class="message">Hi <%=h @person.full_name %>, <br>This is your
name as it will be displayed in eNotify reports. Only you can change
it. <br><br>If you want to show
up with a different name. Now's your big chance. Just click <%= link_to
'Edit my name!', edit_person_path(@person) %>
</div>

<p>
  <b>Username:</b>
  <%=h @person.username %>
</p>

<p>
  <b>First name:</b>
  <%=h @person.first_name %>
</p>

<p>
  <b>Last name:</b>
  <%=h @person.last_name %>
</p>




                                   103
app/views/projections/edit.html.erb
Edit Projections for
<p> <%=h @team.manager.full_name %>'s team</p>
<p>Bug List: <%=h @bug_list.display_name %></p>
<p>Project: <%=h @team.project.name%></p>



<% form_for :projections, :url => { :action => :update } do |form| %>
        <% @projections.each do |projection| %>
                       <p><%= form.text_field projection.date,
projection.bug_count %></p>
        <% end %>
<BR><BR>
<%= form.submit "update projections" %>

<%= link_to("Never mind", :controller => "teams", :action => "show",
:id => @team.id) %>


<% end %>


<table border ="2">
        <tr><th>Date</th><th>Projected Bug Count</th></tr>
<% @projections.each do |projection| %>
        <tr><td><%= projection.date %></td><td><%= projection.bug_count
%></td></tr>

<% end %>




                                      104
app/views/projections/edit_group.html.erb
<div class="message"><span class="label_title">Edit projections for bug
list: <%=h @bug_list.display_name %></span><br><br>

Enter in new projections below. If you don't want projections to show
for a specific date, enter the number -1
</div>
<br

<% form_for( :projection, :url => { :action => "save_group" }) do
|form| %>
<%= form.submit "update projections" %>

<%= link_to("Never mind", :controller => "projections", :action =>
"show", :id => @team.id, :team_id => @team.id, :bug_list_id =>
@bug_list.id) %>

        <% @projections.each do |projection| %>
                       <p><%=h projection.date.to_s %> <%=
form.text_field( projection.date.to_s, {:value => projection.bug_count
}) %></p>
        <% end %>
<BR><BR>
        <%= form.hidden_field('team_id', {:value => @team.id}) %>
        <%= form.hidden_field('bug_list_id', {:value => @bug_list.id})
%>
<%= form.submit "update projections" %>

<%= link_to("Never mind", :controller => "projections", :action =>
"show", :id => @team.id, :team_id => @team.id, :bug_list_id =>
@bug_list.id) %>


<% end %>




                                       105
app/views/projections/index_for_bug_list.html.erb
<div class="message"><span class="label_title">Projections by bug
list</span><br><br>
Click on a bug list to view that team's projections for bug list <%=h
@bug_list.display_name %>.

</div>


<% @bug_list.project.teams.each do |team| %>

               <p><%= link_to(team.manager.full_name, :controller =>
"projections", :action => "show", :id => team.id, :team_id => team.id,
:bug_list_id => @bug_list.id) %> </p>

<% end -%>




                                       106
app/views/projections/index_for_team.html.erb
<div class="message"><span class="label_title">Projections by bug
list</span><br><br>
Click on a bug list to view <%=h @team.manager.full_name %>'s team's
projections for that bug list.

</div>
<ul>
<% @team.project.bug_lists.each do |bug_list| %>

        <li>   <p><%= link_to(bug_list.display_name, :controller =>
"projections", :action => "show", :id => @team.id, :team_id =>
@team.id, :bug_list_id => bug_list.id) %> </p>

<% end -%>
</ul>




                                      107
app/views/projections/show.html.erb
<div class="message"><span class="label_title">Projections for bug
list: <%=h @bug_list.display_name %></span><br><br>
<%= link_to("Edit These Projections", :controller => "projections",
:action => "edit_group", :id => @team.id, :team_id => @team.id,
:bug_list_id => @bug_list.id) %>

</div>
<br>
<table border ="2">
        <tr><th>Date</th><th>Projected Bug Count</th></tr>
<% @projections.each do |projection| %>
        <tr><td><%= projection.date %></td><td><%= projection.bug_count
%></td></tr>

<% end %>




                                      108
app/views/projects/edit.html.erb
<%= error_messages_for :project %>

<%= render :active_scaffold => 'projects', :constraints => {:id =>
params[:id]} -%>




                                     109
app/views/projects/index.html.erb
<%= render   :active_scaffold => "projects"   %>

<script>

<% @projects.each do |p| %>
        <% if !p.is_logged_in_project_administrator? %>
        document.getElementById('projects-edit-<%= p.id.to_s %>-
link').innerHTML = '';
        <% end -%>
<% end -%>

</script>




                                    110
app/views/projects/show.html.erb
<%if @project.setup_required? %>
<div class="message">
        <div id="setup_required">
        You have a little more setup to do before this enotify site will
generate bug reports. Here's what you need to do:
        <ol>
                <%if !@project.has_teams? %>
                        <li> You need to <%= link_to('add at least 1
Team', :id => @project.id, :controller => :teams, :action => :new) %>.
                        </li>
                <% end -%>
                <%if !@project.has_bug_lists? %>
                        <li> You need to <%= link_to('add at least 1 Bug
List', :id => @project.id, :controller => :bug_lists, :action => :new)
%>.</li>
                <% end %>
                <li> That's it!</li>
        </ol>
        Once you're done, you need to activate the site before it will
generate bug reports. Click Edit, and
        set active to "True"

        </div></div>
<% end -%>

<%if !@project.setup_required? && false == @project.active %>
<div class="message">
        <div id="inactive_site">
                This site is currently inactive. That means it will not
generate any bug reports. This is a good thing
                if you don't need the bug reports to update because it
frees up resources and allows other sites to update
                faster.
                <br><br>
                If you want this site to generate click "Edit", and set
Active to "True".

        </div>
        </div>
<% end -%>
<%= render :active_scaffold => 'projects', :constraints => {:id =>
@project.id} -%>

<script>
document.getElementById('mod_actions').innerHTML = '';
<% if !@project.is_logged_in_project_administrator? %>
document.getElementById('projects-edit-<%= @project.id.to_s %>-
link').innerHTML = '';
<% end -%>
</script>




                                   111
app/views/teams/for_project.html.erb
<%= render :active_scaffold => 'teams', :constraints => {:project_id =>
@project.id} -%>


<script>

<% if !@project.is_logged_in_project_administrator? %>
        <% @project.teams.each do |team| %>
                document.getElementById('teams-destroy-<%= team.id.to_s
%>-link').innerHTML = '';
        <% end -%>
<% else -%>
document.getElementById('mod_actions').innerHTML = '<a
href="/teams/new/<%= @project.id.to_s %>" class="new action"
id="teams-new--link" position="top">Add a new team!</a>' +
document.getElementById('mod_actions').innerHTML;
<% end -%>
</script>




                                       112
app/views/teams/new.html.erb
<%= error_messages_for :team %>
<div class="message">
To add a new team to project <%=h @project.name %> enter the username
for the person that will
be the team lead for that team. Hint: the username is the same as their
email username.
<BR><BR>
If the team lead has direct reports, this wizard can automatically
populate those
employees into the new team that is created. If you would like to auto
populate this team,
make sure to check the "Populate Team from Directory" check box.
</div>      <BR><BR>

<% form_for :team, :url => { :action => :create } do |form| %>


<p>Team Lead Username: <%= form.text_field 'username' %> </p>
<p> Populate Team from Directory: <%= form.check_box "use_directory",
{}, "yes", "no" %></p>
<%= form.hidden_field('project_id', {:value => @project.id}) %>

<BR><BR>
  <%= form.submit "Create Team" %>

   <%= link_to("Never mind", :controller => "teams", :action =>
"for_project", :id => @project.id) %>



<% end %>




                                     113
app/views/teams/show.html.erb


<%= render :active_scaffold => "owners", :constraints => {:team_id =>
params[:id]} -%>


<script>
document.getElementById('mod_actions').innerHTML = '<a
href="/owners/new/<%= @team.id.to_s %>" class="new action" id="teams-
new--link" position="top">Add a new Team Member!</a>' +
document.getElementById('mod_actions').innerHTML;

</script>




                                  114
                                  Perl (.pm)



generator/Enotify/Generator/ControllerCommunicator.pm
package Enotify::Generator::ControllerCommunicator;
=head1 Enotify::Generator::ControllerCommunicator

  This package enables communication with the enotify controller
  web application.

  The functionality in this package supports the communication model
  where the generator ALWAYS initiates contact with the Controller.
  The process flow is as follows

  1) a cron task kicks off the generator
  2) the generator asks the controller what work to do
  3) the controller responds with the name of a project
  4) the generator generates the site & sends status messages
     to the controller during the update process
  5) the generator finishes the site update, notifies the controller
and exits


  the cron tab is on a 2 minute retry, with logic to keep multiple
generators
  from starting. so step 1 will happen at most 2 minutes after step 5
completes


  The type of messages the generator will send to the controller


  REQUESTS:
  * Give me a site name wito generate get_site
  * What work am I doing (supported by the controller for future
implementation on the generator)

  Updates:
  * GENERATING SITE -> STARTED
  * GENERATING SITE -> COMPLETE
  * BUG LIST DOWNLOAD -> STARTED
  * BUG LIST DOWNLOAD -> FAILED
  * BUG LIST DOWNLOAD -> COMPLETED
  * RENDERING PAGES -> STARTED
  * RENDERING PAGES -> COMPLETE
  * RENDERING GRAPHS -> STARTED
  * RENDERING GRAPHS -> COMPLETE


      The controller identifies all requests based on the IP address of
the
      generator which is passed with every call.




                                      115
   Deployment Requirement:
     Because it is technally possible for a DHCP generator to change
it's IP
     address while running a site, the enterprise deployment of this
mechanism
     requires generators with Static IP addresses.


   Example use:

    $controller_comunicator =
Enotify::Generator::ControllerCommunicator::new();

       $site_name = $controller_comunicator->get_site_to_generate()

       print "Site Name " . $site_name;

       >Site Name foo

       $controller_comunicator->

=cut

use strict;
use warnings;


#use FLUBBER::Utils::SimpleLog qw (:all);
use LWP::Simple;   ## needed for HTTP access
use LWP::UserAgent; ## needed for HTTP authentication access
use HTTP::Request::Common;
use IO::Socket::INET;
use Sys::Hostname;

use XML::Simple; # needed for parsing responses fromt the controller
use Data::Dumper;




sub new
{
    my ( $test_mode ) = @_;

    if (!defined($test_mode) )
    {
        print("YOU NEED TO DEFINE TEST MODE. new(1) means test mode,
new(0) means production\n";
        return 0;
    }

       my $self = {};
       $self->{IP_ADDRESS} = lookup_local_ip_address();
       $self->{HOSTNAME} = hostname();
       $self->{GEN_PATH} = 'generation.xml';
       $self->{TEST_MODE} = $test_mode;


                                      116
       $self->{USER_AGENT} = LWP::UserAgent->new;
       $self->{USER_AGENT}->timeout(10);

       bless $self;

       return $self;
}

=head2 ControllerCommunicator->get_site_to_generate() {{{

       Purpose: This object encapsulates the details of
                a bug ownership change. This object will
                be used inside a bug in order to change the
                owner of that bug.

       Expects: Nothing. Seriously, it is the controllers responcibility
                to deal with generators that go crazy or ask for this
                info multiple times.

       Returns: (STRING) The short name of the site that is being updated.
This
                is also the last directory in the path to the
                directory for the site.

             "enotify/sites/" . $controller_comunicator-
>get_site_to_generate()

              > enotif/sites/foo
=cut
sub get_site_to_generate
{
     my ($self) = @_;

       my $result = $self->make_controller_post('generations.xml');

       if ($result == 0)
       {
            return 0;
       }
       else
       {
            # parse the response for the short name
            my $response_XML_reference = XMLin($result);
            my $xml_as_hash = Dumper($response_XML_reference);

           return $xml_as_hash;
       }


} # }}}

sub update_generation_started
{
    my ($self) = @_;
    $self->make_controller_put($self->get_gp(), "Started generating the
site");
}


                                      117
sub update_generation_completed
{
    my ($self) = @_;
    $self->make_controller_put($self->get_gp(), "Completed generating
the site");
}


sub update_buglist_download_start
{
    my ($self) = @_;
    $self->make_controller_put($self->get_gp(), "started buglist
download");
}




sub get_gp
{
    my ($self) = @_;
    return $self->{GEN_PATH};
}

=head2 lookup_local_ip_address    {{{

      Purpose: get the local ip addres

      Returns: (STRING) the ip address.

      Example: "192.168.1.1"

=cut
sub lookup_local_ip_address
{
     my ($self) = @_;
     my $sock = IO::Socket::INET->new(
                             PeerAddr=> "example.com",
                             PeerPort=> 80,
                             Proto   => "tcp");
     return $sock->sockhost;

}    # }}}




=head2 $self->make_controller_post() {{{

      Purpose: (INTERNAL ONLY) This method encapulates the mechanics
               of calling the controller with a post a.k.a create message

      Expects: The request path to call on the controller.
               This object already knows the base URL for the controller,
so
               you only need to pass the second half of the URL.


                                        118
             For example: if the URL to get a new site to generate is
                             http://0.0.0.0:3000/generations.xml
                          then you should call
                          $self-
>make_controller_post("generations.xml");

      Returns: (STRING) OR (FALSE/0)

               If the request to the controller fails this method returs
false (0)
               Otherwise it returns the body of the response message.
               This is usually the XML returned by the REST request to
the
               controller.


=cut
sub make_controller_post
{
     my ($self, $request_path) = @_;

      my $response = $self->make_controller_request($request_path, POST);

      if ($response->is_success)
      {
           return $response->decoded_content;
      }
      else
      {
           return 0;
      }


} # }}}

=head2 $self->make_controller_put($request_path, $params) {{{

      Purpose: (INTERNAL ONLY) This method encapulates the mechanics
               of calling the controller with a put a.k.a create message

      Expects: The request path to call on the controller.
               This object already knows the base URL for the controller,
so
               you only need to pass the second half of the URL.

             For example: if the URL to get a new site to generate is
                             http://0.0.0.0:3000/generations.xml
                          then you should call
                          $self-
>make_controller_post("generations.xml");

      Returns: (boolean)
               true: If the request update works
               false (a.k.a 0 zero): if the update fails




                                       119
=cut
sub make_controller_put
{
     my ($self, $request_path, $params) = @_;

    my $response = $self->make_controller_request($request_path, PUT,
$params);

     if ($response->is_success)
     {
          return $response->decoded_content;
     }
     else
     {
          return 0;
     }


} # }}}

=head2 $self->make_controller_request($request_path, $method, $params)
{{{

     Purpose: (INTERNAL ONLY) This method encapulates the mechanics
              of calling the controller with a put a.k.a create message

     Expects:
                (STRING) The request path to call on the controller.
                This object already knows the base URL for the controller,
so
                you only need to pass the second half of the URL.

             For example: if the URL to get a new site to generate is
                             http://0.0.0.0:3000/generations.xml
                          then you should call
                          $self-
>make_controller_request("generations.xml", PUT);

                 (METHOD) PUT or POST


     Returns: (boolean)
              true: If the request update works
              false a.k.a 0 zero: if the update fails


=cut
sub make_controller_request
{
     my ($self, $request_path, $method, $params) = @_;

     my $base_params = [ generator_ip   => $self->{IP_ADDRESS},
                         generator_name => $self->{HOSTNAME} ];

    ### combine the provided params with the default IP and hostname
params
    my $complete_params;


                                        120
    if (!defined($params))
    {
       $complete_params = $base_params;
    }
    else
    {
       $complete_params = [@$params, @$base_params];
    }

    my $ua = LWP::UserAgent->new;
    $ua->timeout(10);

    ## call out to the webserver
    if ($self->{TEST_MODE})
    {
        ## ok just pretend to call out to the server

        my $result = "Calling: " . $method . ' http://0.0.0.0:3000/' .
$request_path . "\n";
        $result .= @$complete_params;

           print $result;
           return '<result>200</result>';
    }
    else
    {
           ## this is the real thing start calling people!!!

        return $response = $self->{USER_AGENT}->request(PUT
'http://0.0.0.0:3000/'
                                                         .
$request_path,

$complete_params);
        if ($response->is_success)
        {
           return $response->decoded_content;
        }
        else
        {
           return 0;
        }

    }




    #my $ua = LWP::UserAgent->new;
    #$ua->timeout(10);
    #return $response = $ua->request(PUT 'http://0.0.0.0:3000/' .
$request_path,
    #                                    $complete_params);

    #                               [ generator_ip   => $self-
>get_local_ip_address,



                                      121
    #                              generator_name => $self-
>get_local_host_name,
    #                              message        => $message,
    #                              bug_list       => $bug_list]);




#(4:04:58 PM) Chris Warren: [@$ref1, @$ref2] would be one way.
#16:05
#(4:05:14 PM) Chris Warren: That would create a new arrayref without
damaging the existing ones.
#(4:05:32 PM) Paul Russell: so $ref1 = [ prop1   => "foo", prop2 =>
"bar"]
#[(@$ref1, @$ref2)] [ prop1    => "foo", prop2 => "bar"]

}    # }}}


1;




                                   122
                                    Ruby (.rb)



app/controllers/administrators_controller.rb
# Controlls the addition and removal of adminnstrators from projects

class AdministratorsController < ApplicationController
  include EnotifyLdap
  layout 'admin_standard'
  before_filter :authenticate
  #before_filter :get_project_context
  active_scaffold :administrator
  active_scaffold :administrator do |config|
       config.columns = [:person]
       config.columns[:person].label = "Administrators"
       config.actions.exclude :update, :show, :create
  end


  def for_project
    @project = Project.find(params[:id])

    respond_to do |format|
      format.html
    end
  end

  # GET /administrators
  # GET /administrators.xml
  def index
    @administrators = Administrator.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @administrators }
    end
  end


  # GET /administrators/new
  # GET /administrators/new.xml
  def new
    @administrator = Administrator.new
    @project = Project.find(params[:id])

    respond_to do |format|
      format.html # new.html.erb
      format.xml { render :xml => @administrator }
    end
  end

  # GET /administrators/1/edit
  def edit



                                        123
    @administrator = Administrator.find(params[:id])
  end

  # POST /administrators
  # POST /administrators.xml
  def create
    @project = Project.find(params[:administrator][:project_id])

    @person_becoming_an_admin =
get_person_from_ldap_by_username(params[:administrator][:username])

    ## the person will be nil if they were not found in LDAP.
    if !@person_becoming_an_admin.nil?

        @person_becoming_an_admin.save

        ## create an populate the administrator object
        @administrator = Administrator.new()
        @administrator.project = @project
        @administrator.person = @person_becoming_an_admin

      respond_to do |format|
        if @administrator.save
          flash[:notice] = @administrator.person.full_name + " has been
added as an administrator for project: " + @project.name
          format.html { redirect_to(:controller => 'administrators',
:action => 'for_project', :id => @administrator.project.id) }
          format.xml { render :xml => @administrator, :status =>
:created, :location => @administrator }
       else
          flash[:notice] = 'Something went wrong.'
          format.html { render :action => "new" }
          format.xml { render :xml => @administrator.errors, :status
=> :unprocessable_entity }

        end
      end #respond_to
    else #if person actuall is nil
      respond_to do |format|
          flash[:notice] = 'The username you provided does not belong to
one of our employees. Do not freak out! This message is NOT endorsed by
the Human Resources Department.'
          format.html { render :action => "new" }
          format.xml { render :xml => @administrator.errors, :status =>
:unprocessable_entity }
      end
    end

  end



  # PUT /administrators/1
  # PUT /administrators/1.xml
  def update
    @administrator = Administrator.find(params[:id])



                                     124
    respond_to do |format|
      if @administrator.update_attributes(params[:administrator])
        flash[:notice] = 'Administrator was successfully updated.'
        format.html { redirect_to(@administrator) }
        format.xml { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml { render :xml => @administrator.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  # DELETE /administrators/1
  # DELETE /administrators/1.xml
  def destroy
    @administrator = Administrator.find(params[:id])

    if @administrator.project.administrators.length <= 1
      flash[:notice] = "You need at least one administrator. If you
really want to delete this admin, please add someone else first."
      redirect_to(:controller => 'administrators', :action =>
'for_project', :id => @administrator.project.id)
    else
      @administrator.destroy

      respond_to do |format|
        format.html { redirect_to(:controller => 'administrators',
:action => 'for_project', :id => @administrator.project.id) }
        format.xml { head :ok }
      end
    end
  end
end




                                   125
app/controllers/application.rb
# Filters added to this controller apply to all controllers in the
application.
# Likewise, all the methods added will be available for all
controllers.

class ApplicationController < ActionController::Base
  include UserInfo
  helper :all # include all helpers, all the time

  before_filter :set_user


  ActiveScaffold.set_defaults do |config|
    # disables dhtml history globally
    config.theme = :blue
  end



  # See ActionController::RequestForgeryProtection for details
  # Uncomment the :secret if you're not using the cookie session store
  protect_from_forgery # :secret => '9a45b429412f6a2c4d6cb6d46dfe46a1'


  protected
  def authenticate
   unless session[:username]
      session[:original_uri] = request.request_uri
      redirect_to :controller => "login"
      return false
   end
  end

  # Sets the current user into a named Thread location so that it can
be accessed
    # by models and observers
    def set_user
      UserInfo.current_user = session[:username]
    end


end




                                   126
app/controllers/bug_columns_controller.rb
class BugColumnsController < ApplicationController
  # GET /bug_columns
  # GET /bug_columns.xml
  def index
    @bug_columns = BugColumn.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @bug_columns }
    end
  end

  # GET /bug_columns/1
  # GET /bug_columns/1.xml
  def show
    @bug_column = BugColumn.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml { render :xml => @bug_column }
    end
  end

  # GET /bug_columns/new
  # GET /bug_columns/new.xml
  def new
    @bug_column = BugColumn.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml { render :xml => @bug_column }
    end
  end

  # GET /bug_columns/1/edit
  def edit
    @bug_column = BugColumn.find(params[:id])
  end

  # POST /bug_columns
  # POST /bug_columns.xml
  def create
    @bug_column = BugColumn.new(params[:bug_column])

    respond_to do |format|
      if @bug_column.save
        flash[:notice] = 'BugColumn was successfully created.'
        format.html { redirect_to(@bug_column) }
        format.xml { render :xml => @bug_column, :status => :created,
:location => @bug_column }
      else
        format.html { render :action => "new" }




                                      127
        format.xml { render :xml => @bug_column.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  # PUT /bug_columns/1
  # PUT /bug_columns/1.xml
  def update
    @bug_column = BugColumn.find(params[:id])

    respond_to do |format|
      if @bug_column.update_attributes(params[:bug_column])
        flash[:notice] = 'BugColumn was successfully updated.'
        format.html { redirect_to(@bug_column) }
        format.xml { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml { render :xml => @bug_column.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  # DELETE /bug_columns/1
  # DELETE /bug_columns/1.xml
  def destroy
    @bug_column = BugColumn.find(params[:id])
    @bug_column.destroy

    respond_to do |format|
      format.html { redirect_to(bug_columns_url) }
      format.xml { head :ok }
    end
  end
end




                                   128
app/controllers/bug_lists_controller.rb
# controlls the metadata associaated with each bug list.

class BugListsController < ApplicationController
  layout 'admin_standard'
  before_filter :authenticate
  before_filter :get_project_context

  active_scaffold :bug_list
  active_scaffold :bug_list do |config|

      config.update.link.label = "View/Edit"
      config.actions.exclude :show
      #config.show.link.page = true
      config.list.per_page = 100
      #show.columns = [:display_name, :display_name_color,
:description, :expert_query, :ownership_model, :use_projections,
:use_ownership_change, :is_verification_list]
      #config.show.link.action :show
      #config.show.type :record
      config.columns = [:display_name, :description, :expert_query,
:ownership_model, :use_projections, :use_ownership_change,
:is_verification_list]
      update.columns = [:display_name, :name, :display_name_color,
:description, :expert_query, :ownership_model, :use_projections,
:use_ownership_change, :is_verification_list]
      config.columns[:ownership_model].form_ui = :select
      config.columns[:description].form_ui = :textarea
      config.columns[:description].options = { :cols => 100, :rows =>
10 }

        config.columns[:expert_query].form_ui = :textarea
        config.columns[:expert_query].options = { :cols => 100, :rows =>
10 }
         config.actions.exclude :show
        #config.actions << :sortable
        #config.sortable.column = :position


  end


  def get_project_context
    if( !params[:id].nil? )
      @project = BugList.find(params[:id]).project
    end
  end




  # GET /bug_lists
  # GET /bug_lists.xml
  def index



                                          129
    @bug_lists = BugList.find(:all)




    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @bug_lists }
    end
  end

  def before_create_save(record)
    # write out the bug_list xml file so they show up for the
generators

    record.name = 'bug_list_' +   record.project.bug_lists.length.to_s

  end



  def after_create_save(record)
    ## add the internal name to be used by the generator
    ## it would be nice if this had some when read by a human, but an
ID is ok

    # write out the bug_list xml file so they show up for the
generators
    # this doesn't work because the record doesn't know it's project
yet
    Project.find(record.project_id).publish_bug_lists
  end

  # GET /projects/1
  # GET /projects/1.xml

  def after_update_save(record)
    # write out the bug_list xml file so they show up for the
generators
    record.project.publish_bug_lists
  end

  def for_project
    @project = Project.find(params[:id])

    #active_scaffold_config.action_links.add "new\/" +
@project.id.to_s, :label => 'Add New Team'
    #{ render :active_scaffold => @projects, :constraints =>
{:project_id => params[:id]} }
    respond_to do |format|
      format.html
    end
  end


=begin
  def list


                                      130
    @bug_list = BugList.find(params[:id])

    active_scaffold_config.label = "Project >> " +
@bug_list.project.name + " >> Bug Lists"

    @bug_lists = Buglist.find_all()

     { render :active_scaffold => @projects }

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @bug_lists }
    end
  end

  # GET /bug_lists/1
  # GET /bug_lists/1.xml
  def show
    @bug_list = BugList.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml { render :xml => @bug_list }
    end
  end

  # GET /bug_lists/new
  # GET /bug_lists/new.xml
  def new
    @bug_list = BugList.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml { render :xml => @bug_list }
    end
  end

  # GET /bug_lists/1/edit
  def edit
    @bug_list = BugList.find(params[:id])
  end

  # POST /bug_lists
  # POST /bug_lists.xml
  def create
    @bug_list = BugList.new(params[:bug_list])

    respond_to do |format|
      if @bug_list.save
        flash[:notice] = 'BugList was successfully created.'
        format.html { redirect_to(@bug_list) }
        format.xml { render :xml => @bug_list, :status => :created,
:location => @bug_list }
      else
        format.html { render :action => "new" }
        format.xml { render :xml => @bug_list.errors, :status =>
:unprocessable_entity }


                                      131
      end
    end
  end

  # PUT /bug_lists/1
  # PUT /bug_lists/1.xml
  def update
    @bug_list = BugList.find(params[:id])

    respond_to do |format|
      if @bug_list.update_attributes(params[:bug_list])
        flash[:notice] = 'BugList was successfully updated.'
        format.html { redirect_to(@bug_list) }
        format.xml { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml { render :xml => @bug_list.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  # DELETE /bug_lists/1
  # DELETE /bug_lists/1.xml
  def destroy
    @bug_list = BugList.find(params[:id])
    @bug_list.destroy

     respond_to do |format|
       format.html { redirect_to(bug_lists_url) }
       format.xml { head :ok }
     end
  end
=end
end




                                   132
app/controllers/generations_controller.rb
# This class is responcible for all interagtions with the generators.
# It contains the defition of the APIs that can be called by the
# generators.

class GenerationsController < ApplicationController




  # POST /generations
  # POST /generations.xml
  def create
    #@generation = Generation.new(params[:generation])

    ## check if there is a generator by this IP
    @generator =
Generator.find_or_create_by_ip_address(params[:generator_ip])

    # set the name if you don't have it
    if @generator.name.nil?
      logger.info "Setting generator name for generator at " +
params[:generator_ip]
      @generator.name = params[:generator_name]
      @generator.save
    end


    # bounce this generator if we've disabled it.
    if @generator.blocked
      logger.warn "Blocked Generator Site Request Denied"

      if !params[:generator_ip].nil?
        logger.warn "Blocked Generator IP address: " +
params[:generator_ip]
      end
      if !@generator.name.nil?
        logger.warn "Blocked Generator Name: " + @generator.name
      end

         respond_to do |format|
           format.xml{ render :xml => '<error>you are generator non
gratta. Please go away</error>', :status => 422 }
         end
    else
      ## this is a valid generator. give it some work.

    ## get the generation this site was last working on.
    logger.info "Getting current generation"
    @generation = @generator.current_generation

    if !@generation.nil? && !@generation.complete
      ## This generator never finished it's job the first time
      ## assign that job again



                                        133
      logger.warn "Rogue Generator requested site before completing
previous Job"
      logger.warn "Rogue Generator Previous Job: "
@generation.project.name

      if !@generator.ip_address.nil?
         logger.warn "Rogue Generator IP address: " +
params[:generator_ip]
      end
      if !@generator.name.nil?
         logger.warn "Rogue Generator Name: " + @generator.name
      end
    else
      ## we need a new generation. the last one was finished
      @generation = Scheduler.instance.schedule_next(@generator,
logger)
    end

      respond_to do |format|
        if !@generation.nil?

          logger.info "PAY ATTENTION " + @generation.project.name
          logger.info "generator IP: " + params[:generator_ip]
          logger.info "generator name:" + params[:generator_name]

          #@generation = Generation.find(@generation.id, :include =>
[:project])
          #logger.info @generation.project.name

          # save a reference to the current generation in the generator
          # it will be updated the next time the generator runs
          @generator.current_generation_id = @generation.id
          @generator.save
          flash[:notice] = 'Generation was successfully created.'
          logger.info "PAY ATTENTION 2" + @generation.project.name
          format.html { redirect_to(@generation) }
          format.xml { render :xml => @generation.to_xml(:include =>
:project, :status => :created, :location => @generation) }
        else
          format.html { render :action => "new" }
          ## return a 501 SERVICE NOT IMPLEMENTED MESSAGE SINCE WE
DIDN'T greate a generations
          format.xml { render :xml => "<NOTHING>There are no sites to
generate now. Please come back later</NOTHING>", :status => 501 }

        end
      end
    end
  end



  # PUT /generations/1
  # PUT /generations/1.xml
  def update
    ## check if there is a generator by this IP


                                   134
    @generator =
Generator.find_or_create_by_ip_address(params[:generator_ip])

    # bounce this generator if we've disabled it.
    if @generator.blocked
         respond_to do |format|
           format.xml{ render :xml => '<error>you are generator non
gratta. Please go away</error>', :status => 422 }
         end
    else

      #logger.info 'generatorxml ip' + params[:ip].to_s
      logger.info "Generator ip " + params[:generator_ip].to_s
      action_sent_by_generator = params[:id].to_s



      @generation = @generator.current_generation

      unknown_action_from_generator = false

      case action_sent_by_generator
      when 'start'
        ## the gerator is starting
        @generation.start_time = Time.now
      when 'end'
        #the generation has ended
        @generation.end_time = Time.now
        @generation.complete = true
      when 'bug_list_download_start'
        #starting a bug list download
        @generation.bug_list_download_start_time = Time.now
      when 'bug_list_download_end'
        # done downloading a bug list
        @generation.bug_list_download_end_time = Time.now
      when 'rendering_pages_start'
        #started rendering the site
        @generation.rendering_pages_start_time = Time.now
      when 'rendering_pages_end'
        # finished rendering the pages.
        @generation.rendering_pages_end_time = Time.now
      when 'rendering_graphs_start'
        #started rendering the graphs
        @generation.rendering_graphs_start_time = Time.now
      when 'rendering_graphs_end'
        # finished rendering the graphs.
        @generation.rendering_graphs_end_time = Time.now

      else
        ## the remote generator said something I don't understand.

        unknown_action_from_generator = true
        respond_to do |format|
          format.html
          format.xml { render :xml => '<error>I don\'t understant what
you said.</error>', :status => 422 }
        end


                                   135
      end

      unless unknown_action_from_generator
        if @generation.save
         respond_to do |format|
             format.html
             format.xml { render :xml => "got it: " +
action_sent_by_generator, :status => 200 }
          end
        else
            respond_to do |format|
               format.html
               format.xml { render :xml => '<error>Couldn\' save the
update time</error>', :status => 422 }
             end
        end

      end
    end
  end

  # DELETE /generations/1
  # DELETE /generations/1.xml
  def destroy
    @generation = Generation.find(params[:id])


    respond_to do |format|
      format.html { redirect_to(generations_url) }
      format.xml { head :ok }
    end
  end

end




                                   136
app/controllers/graphs_controller.rb
# controls the rendering of real time graphs
class GraphsController < ApplicationController



  # GET
/graphs/?project=[projectname]&bug_list=bug_list_0&team=[manager_userna
me]
  # manager_username is optional. if left out, the project projections
are displayed
  ## This action is called from the generator and displays a graph of
the history
  ## projections and current count.
  ##
  ## because the generator (which doesn't know about our id fields) is
asking for the page,
  ## it has to use names like the short project and short bug list name
  def show
      @project = Project.find_by_short_name(params[:project])
      @bug_list = BugList.find(:first, :conditions => { "name" =>
params[:bug_list], :project_id => @project.id } )

      if !params[:team].nil?
          manager = Person.find_by_username(params[:team])
          if !manager.nil?
            @team = Team.find(:first, :conditions => { :person_id =>
manager.id, :project_id => @project.id } )
          end
          ## this isn't finished yet.
      end
      if !params[:owner].nil?
        @owner = Person.find_by_username(params[:owner])
      end

    respond_to do |format|
      format.html # show.html.erb
      format.xml { render :xml => @bug_list_downloads }
    end
  end



end




                                       137
app/controllers/login_controller.rb
# handels login in the development and test environment

class LoginController < ApplicationController
  def index
    # show login screen
  end

  def login
    if !params[:login][:username].nil?
      session[:username] = params[:login][:username]
      redirect_to session[:original_uri]
      #redirect_to :controller => "projects"
    else
      flash[:notice] = 'Invalid username or password.'
      redirect_to :action => "index"
    end
  end

  def logout
    session[:username] = nil
    flash[:notice] = 'You have successfully logged out.'
    redirect_to :action => "index"
  end


end




                                      138
app/controllers/owners_controller.rb
# provides controlls and UI for the addition of owners to a project
team
class OwnersController < ApplicationController
  include EnotifyLdap
  layout 'admin_standard'
  before_filter :authenticate

  active_scaffold :owner
  active_scaffold :owner do |config|
      config.columns = [:owner_name]
      config.list.per_page = 100
      config.list.sorting = [{ :person => :desc}]
      config.actions.exclude :update, :show, :create
      config.columns[:owner_name].label = "Team Members"
  end


  # GET /owners
  # GET /owners.xml
  def index
    @owners = Owner.find(:all )

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @owners }
    end
  end

  # GET /owners/1
  # GET /owners/1.xml

  def show
    @owners = Owner.find_all_by_project_id(params[:id])

    respond_to do |format|
      format.html { render :scaffold => @owners }# show.html.erb
      format.xml { render :xml => @owner }
    end
  end

  # GET /owners/new
  # GET /owners/new.xml
  def new
    @owner = Owner.new
    @team = Team.find(params[:id])
    @project = @team.project
    respond_to do |format|
      format.html # new.html.erb
      format.xml { render :xml => @owner }
    end
  end

  # GET /owners/1/edit



                                       139
  def edit
    @owner = Owner.find(params[:id])
  end

  # POST /owners
  # POST /owners.xml
  def create
      logger.info 'stared create'

        @team = Team.find(params[:owner][:team_id])
        @project = @team.project

      logger.info 'stared person find'
      ## get the person entity
      #@person =
Person.find_or_create_by_username(params[:owner][:username])
      @person =
get_person_from_ldap_by_username(params[:owner][:username])

        ## the person will be nil if they were not found in LDAP.
        if !@person.nil?
          @person.save

          logger.info 'stared making owner'
          ## add the pointers to the team and person that make up this
owner
          @owner = Owner.new
          @owner.team = @team
          @owner.person = @person


          logger.info 'respond_to'

        respond_to do |format|
          logger.info 'start owner save'
          if @owner.save
             logger.info 'Owner saved'
             flash[:notice] = 'Added ' + params[:owner][:username].to_s
+ ' to ' + @team.manager.full_name + "'s team."
             logger.info "flash added"
             format.html { redirect_to(:controller => '/teams', :action
=> :show, :id => @team.id) }
          else
             flash[:notice] = 'Something went wrong.'
             format.html { render :action => "new" }
          end
        end
      else #if person actuall is nil
        respond_to do |format|
            flash[:notice] = 'The username you provided does not belong
to one of our employees. Do not freak out! This message is NOT endorsed
by the Human Resources Department.'
            format.html { render :action => "new" }
            format.xml { render :xml => @administrator.errors, :status
=> :unprocessable_entity }
        end
      end


                                     140
  end

  # PUT /owners/1
  # PUT /owners/1.xml
  def update
    @owner = Owner.find(params[:id])

    respond_to do |format|
      if @owner.update_attributes(params[:owner])
        flash[:notice] = 'Owner was successfully updated.'
        format.html { redirect_to(@owner) }
        format.xml { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml { render :xml => @owner.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  # DELETE /owners/1
  # DELETE /owners/1.xml
  def destroy

      @owner = Owner.find(params[:id])
      @team = @owner.team
      @owner.destroy

    flash[:notice] = 'Owner was successfully deleted. Please Refresh
the page.'

    respond_to do |format|
      format.html { redirect_to(:controller => '/teams', :action =>
:show, :id => @team.id) }
      format.xml { head :ok }
    end
  end

end




                                     141
app/controllers/people_controller.rb
# controlls the UI for users to chang their first and last names

class PeopleController < ApplicationController
  layout 'admin_standard'
  before_filter :authenticate
  active_scaffold :person
  active_scaffold :person do |config|
        config.columns = [:first_name, :last_name]
        config.actions.exclude :update, :create
   end

  # GET /people
  # GET /people.xml
  def index
    @people = Person.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @people }
    end
  end

  ##
  # Display the current user
  def show
     @person = Person.find_by_username(params[:username])

    if @person.nil?
        @person = Person.new
        @person.username = params[:username]
        @person.save
    end

    respond_to do |format|
      format.html # show.html.erb
      format.xml { render :xml => @person }
    end
  end

  ##
  # Allows the user to view the edit screen for his or her name.
  # The currently logged in user can only get to their own edit page.
  def edit
     @person = Person.find(params[:id])

    if @person.username != session[:username]
       flash[:notice] = "Stop messing with other people's names. It's
rude."
       redirect_to( :controller => :people, :username =>
@person.username, :action => :show )
    end

  end



                                       142
  ##
  # Allows the user to change his or her name.
  # The currently logged in user can only edit their own name.
  def update
     @person = Person.find(params[:id])

   if @person.username != session[:username]
         flash[:notice] = "Stop messing with other people's names. It's
rude."
         redirect_to( :controller => :people, :username =>
@person.username, :action => :show )
   else
       respond_to do |format|
         if @person.update_attributes(params[:person])
           flash[:notice] = 'Your name was successfully updated.'
           format.html { redirect_to( :controller => :people, :username
=> @person.username, :action => :show ) }
           format.xml { head :ok }
         else
           format.html { render :action => "edit" }
           format.xml { render :xml => @person.errors, :status =>
:unprocessable_entity }
         end
       end
    end
  end


end




                                   143
app/controllers/projections_controller.rb
# controlls the display and editing of team/buglist projections

class ProjectionsController < ApplicationController
  layout 'admin_standard'
  before_filter :authenticate

  ##
  # Show the projections for a given team
  def index_for_team
     @team = Team.find(params[:id])

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @projections }
    end
  end

  ##
  # Show the projections for a given bug list
  def index_for_bug_list
     @bug_list = BugList.find(params[:id])

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @projections }
    end
  end

  ##
  # Show the projections for a given team and bug list.
  # Expects to recieve both a team and a bug list.
  def show

    @team = Team.find(params[:team_id])
    @bug_list = BugList.find(params[:bug_list_id])
    @projections =
Projection.find_projections_by_team_and_bug_list(@team, @bug_list)

    respond_to do |format|
      format.html # show.html.erb
      format.xml { render :xml => @projection }
    end
  end



  ##
  # Display the edit page for projections for the given team and bug
list
  # will only provide the edit page if the current logged in user is
permitted
  # to edit these projections.
  def edit_group



                                            144
    #Projection.find(:all, :conditions => {:team_id =>
params[:team_id], :bug_list_id => params[:bug_list_id] } )
     @team = Team.find(params[:team_id])
     @bug_list = BugList.find(params[:bug_list_id])
     @projections =
Projection.find_projections_by_team_and_bug_list(@team, @bug_list)
     @projection = Projection.new

      if @team.manager.username == session[:username] ||
@team.project.is_logged_in_project_administrator?
        # there's nothing to see here. this person is allowed. keep
going

          ## find the actual projection dates. These may have changed
          ## if the project start date or projection weeks were changed.
         respond_to do |format|
          format.html # edit.html.erb
          format.xml { render :xml => @projection }
         end

        else
          flash[:notice] = "Stop messing with other team leads
projections. It's rude."
          redirect_to( :controller => :projections, :id => @team.id,
:bug_list_id => @bug_list.id, :team_id => @team.id, :action => :show )
     end



  end



  ##
  # Save the projections for the given team and bug list
  # will only provide the save if the current logged in user is
permitted
  # to edit these projections.
  def save_group
     @team = Team.find(params[:projection][:team_id])
     @bug_list = BugList.find(params[:projection][:bug_list_id])

    if @team.manager.username == session[:username] ||
@team.project.is_logged_in_project_administrator?

        logger.info "saving projections"
        #logger.info params.to_s
        projection_dates = @team.project.projection_dates
        Projection.delete_projections_for_team_and_bug_list(@team,
@bug_list)

        projection_dates.each do |projection_date|
          projection = Projection.new(:team_id => @team.id,
:bug_list_id => @bug_list.id, :date => projection_date, :bug_count =>
params[:projection][projection_date.to_s] )
          projection.save



                                      145
          #logger.info "found param: " + projection_date.to_s + ":" +
params[:projection][projection_date.to_s]
        end


        respond_to do |format|
           if true # @projection.update_attributes(params[:projection])
             flash[:notice] = 'Your projections were successfully
updated.'
             #format.html { redirect_to(:controller => '/teams', :action
=> :show, :id => @team.id) }
             format.html { redirect_to :action => "show", :params => {
:team_id => @team.id, :bug_list_id => @bug_list.id } }
             format.xml { head :ok }
           else
             format.html { render :action => "show", :team_id =>
@team.id, :bug_list_id => @bug_list.id }
             format.xml { render :xml => @projection.errors, :status =>
:unprocessable_entity }
           end
        end
      else
          flash[:notice] = "Stop messing with other team leads
projections. It's rude."
          redirect_to( :controller => :projections, :id => @team.id,
:bug_list_id => @bug_list.id, :team_id => @team.id, :action => :show )
       end
  end


end




                                   146
app/controllers/projects_controller.rb
# controlls the creation, display and editing of projects.
# much of the UI configuration can be found here as the
# majority of the applicaion centers around projects

class ProjectsController < ApplicationController
  layout 'admin_standard'
  before_filter :authenticate
  before_filter :get_project_context
  active_scaffold :project
  active_scaffold :project do |config|
      config.label = ''
      #config.show.link.page = true
      #config.show.link.action :show
      #config.show.type :record
      config.columns = [:name, :short_name, :status,
:site_update_frequency, :teams, :bug_lists, :admins,
                         :projection_start_date, :projection_weeks,
:projection_end_date,
                        :bug_tool_url]
      #config.columns[:teams].label = "Teams"
      config.columns[:active].label = "Active"
      config.columns[:projection_start_date].label = "Start Date"
      config.columns[:projection_weeks].label = "Duration"
      config.columns[:projection_end_date].label = "End Date"

      config.update.columns = [:name, :active, :site_update_frequency,
                         :projection_start_date, :projection_weeks,
                        :bug_tool_url]
      config.create.columns = [:name, :short_name, :active,
:site_update_frequency,
                               :projection_start_date,
:projection_weeks,
                               :bug_tool_url]
      update.columns.exclude :teams, :bug_lists, :admins,
:projection_end_date, :edit_control
      create.columns.exclude :teams, :bug_lists, :admins,
:projection_end_date, :active
      #config.update.columns[:projection_weeks].label = "Duration (in
weeks)"
      config.columns[:site_update_frequency].form_ui = :select
      config.columns[:site_update_frequency].label = "Update Frequency"
      config.columns[:bug_tool_url].form_ui = :select
      #show.link_actions.exclude :create
      config.actions.exclude :delete, :show


       #update.columns.add :table_column_order
       list.sorting = {:site_update_frequency => :desc}

    end




                                         147
  ##
  # used as a before filter to make sure the UI has access to the
project
  def get_project_context
     if( !params[:id].nil? )
       @project = Project.find(params[:id])
     end
  end



  # GET /projects
  # GET /projects.xml
  def index
    @projects = Project.find(:all)


    respond_to do |format|
      format.html { render :active_scaffold => @projects }#
index.html.erb
      format.xml { render :xml => @projects }
    end
  end

  # POST /projects
  # POST /projects.xml
  def before_create_save(record)
      record.active = false
  end

  ##
  # triggered after a project save.
  # Adds the current logged in user as an admin and
  # creates the generated site directory structure for the site
  def after_create_save(record)
     # write out the properties so they show up for the generators

    ## give this new project an adminstrator
    admin = Person.find_or_create_by_username(session[:username])
    administrator = Administrator.new
    administrator.person = admin
    administrator.project = record
    administrator.save

    ## create the site directories
    record.create_site_directory_structure
    record.publish_site_properties
  end

  # GET /projects/1
  # GET /projects/1.xml
  def after_update_save(record)
    # write out the properties so they show up for the generators
    record.publish_site_properties
  end




                                     148
  def show
    @project = nil

      if !params[:site].nil?
        @project = Project.find_by_short_name(params[:site])
      else
        @project = Project.find(params[:id])
      end
      respond_to do |format|
        format.html # show.html.erb
        format.xml { render :xml => @project }
      end

  end

end




                                     149
app/controllers/teams_controller.rb
# controlls the creation deletion and manipulation of teams associated
with projects
class TeamsController < ApplicationController
  include EnotifyLdap

  layout 'admin_standard'
  before_filter :authenticate


  active_scaffold :team

  active_scaffold :team do |config|
       config.label = ''
       config.show.link.page = true
       config.columns = [:manager, :count, :projections]
       config.list.per_page = 100
       config.list.sorting = [{ :person => :desc}]
       config.columns[:person].label = 'Manager'
       config.columns[:count].label = 'Team Members'

        config.actions.exclude :update, :create, :show

  end


  ##
  # show the teams for a project
  def for_project
     @project = Project.find(params[:id])

    respond_to do |format|
      format.html
      format.xml { render :xml => @teams }
    end
  end


  ##
  # show a team
  def show
     @team = Team.find(params[:id])
     @project = @team.project

    respond_to do |format|
      format.html # show.html.erb
      format.xml { render :xml => @team }
    end
  end


  ##
  # display the team creation page
  def new



                                      150
    @team = Team.new
    @project = Project.find(params[:id])


    respond_to do |format|
      format.html # new.html.erb
      format.xml { render :xml => @team }
    end
  end

  # GET /teams/1/edit
  def edit
    @team = Team.find(params[:id])
  end

  # POST /teams
  # POST /teams.xml
  def create
    @username = params[:team][:username]
    @project = Project.find(params[:team][:project_id])
    @manager =
get_person_from_ldap_by_username(params[:team][:username])
    if !@manager.nil?
      @manager.save


      # create and populate the team
      @team = Team.new()
      @team.project = @project
      @team.person = @manager

      #make the manager a member of the team
      @owner = Owner.new
      @owner.team = @team
      @owner.person = @manager


      if params[:team][:use_directory]
        # Lookup the team in LDAP
        team_members =
get_array_of_employees_from_ldap_by_manager_username(@manager.username)

        team_members.each do |team_member|
          # save the team member
          team_member.save

          # Add this person to the team
          owner = Owner.new
          owner.team = @team
          owner.person = team_member
          owner.save
        end

      end




                                     151
      respond_to do |format|
        if @owner.save && @team.save
          flash[:notice] = 'Team was successfully created. username:' +
params[:team][:username].to_s

          format.html { redirect_to(:controller => '/teams', :action =>
:show, :id => @team.id) }
          format.xml { render :xml => @team, :status => :created,
:location => @team }
        else
          flash[:notice] = 'Something went wrong.'

          format.html { render :action => "new" }
          format.xml { render :xml => @team.errors, :status =>
:unprocessable_entity }
        end
      end
    else ## if manager is nill
      respond_to do |format|
          flash[:notice] = 'The username you provided does not belong
to one of our employees. Do not freak out! This message is NOT endorsed
by the Human Resources Department.'

          format.html { render :action => "new" }
          format.xml { render :xml => @team.errors, :status =>
:unprocessable_entity }
      end


    end
  end


  # DELETE /teams/1
  # DELETE /teams/1.xml
  def destroy
    @team = Team.find(params[:id])
    @team.destroy

    respond_to do |format|
      format.html { redirect_to(teams_url) }
      format.xml { head :ok }
    end
  end

end




                                     152
app/helpers/bug_columns_helper.rb
module BugColumnsHelper

  ##
  # takes a string of comma seperated bug_column_ids
  # returns a string of bug column names as they are
  # known by the generators, seperated by comas
  def self.format_columns_for_generator(column_ids)
     generator_formated_bug_columns = ''
     column_ids.split(",").each do |column_application_id|
       generator_formated_bug_columns +=
BugColumn.find_by_application_id(column_application_id.to_s).field_name
       unless column_application_id == column_ids.split(",").last
         generator_formated_bug_columns += ","
       end
     end
     generator_formated_bug_columns
  end

  ##
  # change the look of the description field when editing
  def description_update(record)
     text_area :record, :description, :cols => 25, :rows => 10
  end

end




                                    153
app/helpers/bug_lists_helper.rb
module BugListsHelper

  # change the look of the display_name column
  def display_name_column(record)
    '<font color="#' + record.display_name_color + '">' +
record.display_name + '</font>'
   end

      # change the look and feel of the projections column depending
      # on the logged in user
      def use_projections_column(record)
        if record.use_projections

        if record.project.is_logged_in_project_administrator?
           link_to("view/edit", :controller => "projections", :id =>
record.id, :action => "index_for_bug_list" )
        else
           link_to("view", :controller => "projections", :id =>
record.id, :action => "index_for_bug_list" )
        end
      else
        "off"
      end
    end


end




                                      154
app/helpers/owners_helper.rb
module OwnersHelper

  ##
  # display the full name for the owner
  def person_column(record)
     '<b>' + record.person.full_name_and_username + '</b>'
  end

  ##
  # display the full name for the owner
  def owner_name_column(record)
     '<b>' + record.owner_name + '</b>'
  end


end




                                   155
app/helpers/projects_helper.rb
# this module is responcible for most of the row level formatting
# on the project page.
module ProjectsHelper




  ##
  # creates the link to the project's teams list for the teams column
  def teams_column(record)
     '<b>' + link_to(record.teams.count.to_s + " teams", :controller =>
"teams", :action => 'for_project', :id => record.id) + '</b>'
     ##
http://0.0.0.0:3000/projects/6/nested?_method=get&associations=teams
  end


  ##
  # creates the link to the project's bug lists for the ug lists column
  def bug_lists_column(record)
     '<b>' + link_to(record.bug_lists.count.to_s + " lists", :controller
=> "bug_lists", :action => 'for_project', :id => record.id) + '</b>'
  end

  ##
  # creates the link to the project's admins list for the admins column
  def admins_column(record)
     '<b>' + link_to( pluralize(record.admins.count, 'admin'),
:controller => "administrators", :action => 'for_project', :id =>
record.id) + '</b>'

  end

  ##
  # displays the number of projection weeks
  def projection_weeks_column(record)
     pluralize(record.projection_weeks, 'week')
  end

  ##
  # creates the a clickable link to the projects generated website
  def name_column(record)
     '<b>' + link_to(record.name, :controller => '/sites/' +
record.short_name + '/output/website/index.html' )     + '</b>'
  end


  ##
  # Displays the status of the project
  def status_column(record)

    if record.setup_required?




                                   156
       '<b>' + link_to("Setup Required", {:controller => "projects", :id
=> record.id, :action => "show"}, {:style=>'color:#FF0000;'} )      +
'</b>'
    elsif false == record.active
       link_to("disabled", {:controller => "projects", :id => record.id,
:action => "show"})
    elsif record.generating?
       '<b>' + link_to("Generating", {:controller => "projects", :id =>
record.id, :action => "show"}, {:style=>'color:#00FF00;'})      + '</b>'
    elsif true == record.active
       link_to("Pending Update", {:controller => "projects", :id =>
record.id, :action => "show"})
    end
  end

  ##
  # controlls the display of the edit control based on the
  # permissions of the logged in user

  def edit_control_column(record)
    if record.is_logged_in_project_administrator?
      '<b><a href="http://0.0.0.0:3000/projects/' + record.id.to_s +
'/edit?_method=get" class="edit action" id="projects-edit-' +
record.id.to_s + '-link" position="replace">Edit</a></b>'
    else
      "no soup"
    end
  end

end




                                   157
app/helpers/teams_helper.rb
module TeamsHelper

  ##
  # displays the full name of the manager and makes it a clickable link
  # to the team page that will display all of the owners
  def manager_column(record)
     '<b>' + link_to(record.person.full_name_and_username, :controller
=> "teams", :id => record.id, :action => "show" )     + '</b>'
  end

  ##
  # displays the full name of a person any time it is displayed
  def person_column(record)
     '<b>' + record.person.full_name_and_username + '</b>'
  end

  ##
  # Changes the display of the edit button for projections
  # based on the permissions of the logged in user
  def projections_column(record)
     if record.project.is_logged_in_project_administrator?
       link_to("view/edit projections", :controller => "projections",
:id => record.id, :action => "index_for_team" )
     else
       link_to("view projections", :controller => "projections", :id =>
record.id, :action => "index_for_team" )
     end
  end


end




                                   158
app/models/administrator.rb
class Administrator < ActiveRecord::Base
  belongs_to :project
  belongs_to :person


  # this field is needed in order for the view to be able to
  # accept a username when creating a team
  # rails wants us to use fields from the model, but we really don't
  # want to tie this to the nested model at that point...
  # we need to run a bunch of logic before creating the team object
  def username
  end

end




                                   159
app/models/bug_column.rb
class BugColumn < ActiveRecord::Base
end




                                   160
app/models/bug_list.rb
class BugList < ActiveRecord::Base
  belongs_to :project
  belongs_to :scope_filter
  belongs_to :ownership_model

  validates_presence_of :display_name
  validates_presence_of :expert_query


  ##
  # Tells Active record the name it should use when displaying
  # a bug list is the display_name and not the name field.
  def to_label
     self.display_name
  end

  ##
  # renders the column order as csv. This should be
  # replaced in the future such that columns more
  # visually appealing
  def table_columns_order_as_csv

BugColumnsHelper.format_columns_for_generator(self.table_column_order)
  end
end




                                     161
app/models/bug_list_downloads.rb
class BugListDownloads < ActiveRecord::Base
end




                                   162
app/models/bug_ownership_change.rb
class BugOwnershipChange < ActiveRecord::Base
end




                                     163
app/models/bug_tool_url.rb
class BugToolUrl < ActiveRecord::Base
  has_many :projects
end




                                   164
app/models/generation.rb
class Generation < ActiveRecord::Base
  belongs_to :project
  belongs_to :generator


end




                                   165
app/models/generator.rb
class Generator < ActiveRecord::Base
  has_many :generations
  belongs_to :generation


  # gets the current generation.
  #
  # You could determine this logically from the generations table
  # but if the system every gets out of sync due to a bug (this will,
  # of course never happen) the data. coruption would be catastrphic.
  # So lets not rely on the generations table.
  def current_generation
    if self.current_generation_id.nil?
      logger.info "didn't find a current generation ID"
      return nil
    else
      ## this is no Generation.find( self.current_generation_id)
because active record throws an
      ## exception if it can't find a record with this syntax. It's
possible an over eager
      ## IT OPS person might delete a generation so we don't want to
see exceptions
      return Generation.find(:first, :conditions => { :id =>
self.current_generation_id } )
    end

  end
end




                                   166
app/models/owner.rb
class Owner < ActiveRecord::Base
  belongs_to :team
  belongs_to :person



  # this field is needed in order for the view to be able to
  # accept a username when creating a team
  # rails wants us to use fields from the model, but we really don't
  # want to tie this to the nested model at that point...
  # we need to run a bunch of logic before creating the team object
  def username
  end

  def owner_name
    self.person.full_name_and_username
  end

end




                                   167
app/models/ownership_model.rb
class OwnershipModel < ActiveRecord::Base
  has_many :bug_lists
end




                                   168
app/models/person.rb
class Person < ActiveRecord::Base
  has_many :administrators
  has_many :teams


  ##
  # renders the first, last and username name of the peron
  # in the format Paul Russell
  def full_name
     output = ''
     if !self.first_name.nil?
       output += self.first_name
     end
     if !self.first_name.nil? && !self.last_name.nil?
         output += " "
     end
     if !self.last_name.nil?
       output += self.last_name
     end
     if output == ''
       output += self.username
     end
     output
  end

  ##
  # renders the first, last and username name of the peron
  # in the format Paul Russell (parussel)
  def full_name_and_username
      full_name << ' (' << self.username << ')'
  end

  ##
  # defaults the display of a persons name to their full
  # name and username
  def name
     full_name_and_username
  end

  ##
  # renders the person in csv format
  # this is used to render generator ownership files
  def as_csv
     output = ''
     if !self.username.nil?
       output += self.username
     end
     output += ','
     if !self.first_name.nil?
       output += self.first_name
     end
     output += ','
     if !self.last_name.nil?



                                    169
      output += self.last_name
    end
    output
  end


end




                                 170
app/models/project.rb
# the project class is the central class in this application
# everything revolves around a project.
class Project < ActiveRecord::Base
  include UserInfo
  validates_presence_of :name
  validates_uniqueness_of :short_name
  validates_numericality_of :projection_weeks
  validates_presence_of :site_update_frequency
  validates_presence_of :bug_tool_url
  has_many :generations
  has_many :owners
  has_many :bug_lists
  has_many :administrators
  has_many :admins, :through => :administrators, :source => :person
  has_many :teams
  belongs_to :bug_tool_url
  belongs_to :site_update_frequency

  require 'date'
  require 'rubygems'
        require 'active_support'

        def new
          @project = super
          @project.projection_start_date = today
  end

  # test method
  def respond_already
      publish :the_ear, "this is a project"
  end

  ##
  # determines if the site requres further setup
  # before it can be generated
   def setup_required?
     if !has_teams? || !has_bug_lists?
       true
     else
       false
     end
   end

   ##
   # determines if the project has any teams
   def has_teams?
      if self.teams.count == 0
        false
      else
        true
      end
   end




                                    171
   ##
   # determines if the project has any bug lists
   def has_bug_lists?
      if self.bug_lists.count == 0
        false
      else
        true
      end
   end


   ##
   # only authenticated users are authorized to create records
    def authorized_for_update?
      self.is_logged_in_project_administrator?
    end

       ##
       # determines if the currently logged in user is an admin for this
site
       def is_logged_in_project_administrator?
         return true
         logger.info "checking admin.usernames"
         logger.info "project name"
         self.admins.each do |admin|
           logger.info "checking admin.username: " + admin.username
           if admin.username == current_user
             return true
           end
         end
         logger.info "no match for admin.usernames"
         # this is not the username of this project's admin
         return false
       end


  ##
  # returns a string of the administrators full names.
  # this is used in constructing generator configuration files
  def administrator_full_names
     admin_list = ''

    self.administrators.each do |admin|
      if !admin.person.first_name.nil?
        admin_list += admin.person.first_name + ' '
      end
      if !admin.person.last_name.nil?
        admin_list += admin.person.last_name + ' '
      end
      admin_list += '('+admin.person.username+') '
    end
    admin_list
  end

  ##
  # returns a string of the administrators email addresses.
  # this is used in constructing generator configuration files


                                      172
  def administrator_email_addresses
    emails = ''
    self.administrators.each do |admin|
      emails += admin.person.username + ' '
    end
    emails
  end

  ##
  # returns the projected end date
  def projection_end_date
     projection_weeks.weeks.since(projection_start_date)
  end

  ##
  # return an array of all of the projection dates
  # starting with the project start date and going
  # out as far as the the projection weeks
  def projection_dates
     dates_array = Array.new
     projection_weeks.times do|week_number|
       dates_array.push(week_number.weeks.since(projection_start_date))
     end

    return dates_array
  end

  ##
  # returns the last complete generation
  def latest_complete_generation
     Generation.find(:first, :conditions => {:project_id => self.id,
:complete => true }, :order => 'end_time DESC' )
  end

  ##
  # returns the time of the latest complete generation
  def latest_complete_generation_time
     generation = latest_complete_generation
     unless generation.nil?
       return generation.start_time
     end
     ## there no generation so we can't find a time
     return Time.now - 2.years
  end

  ##
  # Retruns true if the site is currently being generated
  def generating?
     logger.info "GENERATING?"
     last_gen = self.latest_generation
     if !last_gen.nil?
       logger.info "latest_generation.complete" + last_gen.complete.to_s
       if last_gen.complete
         ## we are not currently generating this site
         return false
       else
         #we are currently generating this site


                                   173
         return true
      end
    else
      logger.info "the latest generation was nil"
      return false
    end
  end

  ##
  # returns the latest generation.
  # Warning: this may be a generation in progress
  def latest_generation
     latest_generation = Generation.find(:first, :conditions =>
{:project_id => self.id }, :order => 'start_time DESC' )
  end

  ##
  # returns true if the project should be generated
  def should_generate?

    if !active
      ## never generate if the site is disabled or currently generating
      return false
    end

    if setup_required?
      ## this site might be active, but it requres setup, so don't
generate it.
      return false
    end

    if generating?
      ## don;t generate a site that's already generating
      return false
    end

    unless !latest_complete_generation_time.nil?
      ## if it never generated then it should
      return true
    end

    if site_update_frequency.generator_name.eql?('weekly')
       logger.info "last complete generation time: " +
latest_complete_generation_time.to_s
       if latest_complete_generation_time < Time.now - 1.week
         return true
       end
    elsif site_update_frequency.generator_name.eql?('daily')

logger.info "TESTING DAILY GENERATION TIMES"
      logger.info "last completed daily generation time: " +
latest_complete_generation_time.to_s
      #logger.info "Time.now - 1.day " + Time.now - 1.day
      if latest_complete_generation_time < Time.now - 1.day
        logger.info "Yes Generate " + name
         return true
       end


                                   174
    else
      ## constantly sites should always generate
      return true
    end

    return false;

  end

  ##
  # get the columns as know by the generator in a comma seperated list
  def table_columns_order_as_csv
     ## bug_columns_helper

BugColumnsHelper.format_columns_for_generator(self.table_column_order)
  end

  ##
  # returns the full path to the site director for this project
  def site_directory
   SITES_DIRECTORY + self.short_name + "/"
  end

  def site_config_directory
    site_directory + 'configuration/'
  end

  def site_output_directory
    site_directory + 'output/'
  end

  def site_history_directory
    site_output_directory + 'history/'
  end

  def site_owners_directory
    site_output_directory + 'owner_lists/'
  end

  def site_projections_directory
    site_output_directory + 'projections/'
  end

  def site_history_url
    SITES_URL + self.short_name + "/output/history/"
  end

  def site_website_url
    SITES_URL + self.short_name + "/output/website/"
  end

  ##
  # returns a stirng containing the site properties in the XML
  # format know by the controller
  def site_properties_as_xml

    output = %{<site-properties>


                                   175
    <!--


#######################################################################
#
         ##
##
         ##
##
         ##                       WARNING WARNING WARNING
##
         ##
##
         ##     Do not manyally edit this file! This configuration file
can   ##
         ##     only be updated through the site Administration web
##
         ##     interface which you can access by clicking on "Site
##
         ##     Administration" from the product page of your site.
##
         ##
##
         ##     Any attempt to edit this file manually will result in
data     ##
         ##     loss and this enotify site failing to run in the
future.      ##
         ##
##
         ##
##

#######################################################################
#

    -->
    }

    output += '<property name="site_active" value="' + self.active.to_s
+ '" type="boolean" />' + "\n"
    output += '<property name="site_update_frequency" value="' +
self.site_update_frequency.generator_name.to_s + '" />' + "\n"
    output += '<property name="project_name" value="' +
CGI.escapeHTML(self.name) + '" />' + "\n"
    output += '<property name="projection_start_date" value="' +
self.projection_start_date.to_s + '" type="time" />'    + "\n"
    output += '<property name="projection_status_model_name" value=""
/>' + "\n"
    output += '<property name="number_of_projection_weeks" value="' +
self.projection_weeks.to_s + '" />' + "\n"
    output += '<property name="administrator_full_name" value="'
    self.administrators.each do |admin|
      output += admin.person.full_name_and_username
      unless admin == administrators.last
        output += ', '
      end


                                   176
    end
    output += '"   />' + "\n"
    output += '<property name="administrator_email_address" value="' +
self.administrator_email_addresses + '" />' + "\n"
    output += '<property name="bug_tool_url" value="' +
self.bug_tool_url.url + '" />' + "\n"
    output += '<property name="bug_table_column_order" value="' +
self.table_columns_order_as_csv + '" type="csv"   />'   + "\n"
    output += '<property name="base_url"   value="/"     />'   + "\n"

    # this is left out because sortable isn't going to be an option in
the new generator
    # output += '<property name="site_columns_sortable"
value="'+project.empty+'"     />'

    output += '</site-properties>'
    output
  end

  ##
  # returns a string containing the projects bug lists in the XML
  # format known to the controller
  def bug_lists_as_xml

    output = %{<qddts-queries>

    <!--


#######################################################################
#
         ##
##
         ##
##
         ##                       WARNING WARNING WARNING
##
         ##
##
         ##     Do not manyally edit this file! This configuration file
can   ##
         ##     only be updated through the site Administration web
##
         ##     interface which you can access by clicking on "Site
##
         ##     Administration" from the product page of your site.
##
         ##
##
         ##     Any attempt to edit this file manually will result in
data     ##
         ##     loss and this enotify site failing to run in the
future.      ##
         ##
##
         ##
##


                                     177
#######################################################################
#



    -->
    }

    self.bug_lists.each do |list|
       output += "<query-def \n"
       output += "name=\"" + CGI.escapeHTML(list.name) + "\"\n"
       output += "use_projections=\"" + list.use_projections.to_s +
"\"\n"
       output += "use_ownership_change=\"" +
list.use_ownership_change.to_s + "\"\n"
       output += "is_verification_list=\"" +
list.is_verification_list.to_s + "\"\n"
       output += "ownership_model_name=\"" +
list.ownership_model.legacy_name.to_s + "\"\n"
       output += "scope_filter=\"individual\">\n"

        output += '<display-name color="' + list.display_name_color +
'">' + CGI.escapeHTML(list.display_name) + '</display-name>' + "\n"
        output += '<description>' + CGI.escapeHTML(list.description)
+'</description>' + "\n"
      output += '<expert-query>' + CGI.escapeHTML(list.expert_query)
+'</expert-query>' + "\n"
      output += '<query_bug_table_column_order override="' +
list.override_column_order.to_s + '" value="' +
list.table_columns_order_as_csv + '" />'
      output += "</query-def> \n"

    end

    output += "</qddts-queries> \n"
    output
  end

  ##
  # publishes the site properties XML file to the
  # site directory for this project
  def publish_site_properties
     publish_config_file('site_properties.xml', site_properties_as_xml)
  end

  ##
  # publishes the bug list in query.xml format to
  # the site directory for this project
  def publish_bug_lists
     publish_config_file('queries.xml', bug_lists_as_xml)
  end

  ##
  # publishes the owner lists to the site owners directry.
  def publish_owner_lists
     self.teams.each do |team|


                                      178
      f = File.open(site_owners_directory + team.manager.username +
".txt","w")
      file_content = ''
      team.team_members.each do |team_member|
        file_content += team_member.as_csv + "\n"
      end
      f.puts file_content
      f.close
    end
  end

  ##
  # writes file_contents to file name in the config
  # directory for this project site.
  def publish_config_file(filename, file_content)
     ## make sure the site direcrtory exists
     f = File.open(site_config_directory + filename,"w")
     f.puts file_content
     f.close
  end

  ##
  # publishes the projections for each team and
  # for the overll project
  def publish_projections
     logger.info "publishing projections"
     teams.each do |team|
       team_projection_path = site_projections_directory +
team.manager.username + '_team/'
       FileUtils.mkdir_p(team_projection_path)
       logger.info "publishing for team " + team.manager.full_name
       bug_lists.each do |bug_list|
         if bug_list.use_projections
             logger.info "writing file " + team_projection_path +
bug_list.name + ".txt"

              ## write out the legacy generator projection files
              f = File.open(team_projection_path + bug_list.name +
".txt","w")
              file_content = ''

Projection.find_projections_by_team_and_bug_list(team,bug_list).each do
|projection|
               file_content += projection.as_generator_csv + "\n"
               f.puts file_content
             end
             f.close
        end
      end

    end
  end


  ##
  # creates all the site directories needed
  # by this project, or it's generator


                                     179
  def create_site_directory_structure
    FileUtils.mkdir_p([site_directory,
                       site_config_directory,
                       site_output_directory,
                       site_history_directory,
                       site_owners_directory,
                       site_projections_directory,
                       site_output_directory + 'logs/',
                       site_output_directory + 'query_results/',
                       site_output_directory + 'site_records',
                       site_output_directory + 'website'] )
  end

  ##
  # builds the directory structure and puts
  # all the confi files into it.
  def publish_site
     create_site_directory_structure
     publish_site_properties
     publish_bug_lists
     publish_owner_lists
     publish_projections
  end



end




                                   180
app/models/projection.rb
class Projection < ActiveRecord::Base
  validates_numericality_of :bug_count

  ##
  # returns the projection in the format <epoc time>,<bug count>
  # the legacy generator does not use -1 for missing projections
  # and it adds the team projections together to get the project
projections
  # so we need to change any -1 to 0
  def as_generator_csv
     #Time.parse(self.date.to_s).to_i.to_s + ',' + self.bug_count
     if bug_count < 1
       date.to_time.to_i.to_s + ',0'
     else
       date.to_time.to_i.to_s + ',' + self.bug_count.to_s
     end
  end

  ##
  # renders a projection array as JSON
  # this is used in graphing
  def self.as_js_array(projection_array)
     json = '['

    projection_array.each do |projection|
      json += '[' + projection.date.to_time.to_i.to_s + ',\'' +
projection.bug_count.to_s + '\'],'
    end

    return json += ']'

  end



  ##
  # deletes all of a teams projections
  def self.delete_projections_for_team_and_bug_list(team, bug_list)

    saved_projections = Projection.find(:all, :conditions => {:team_id
=> team.id, :bug_list_id => bug_list.id } )

    saved_projections.each do |projection|
      projection.destroy
    end

  end

  ##
  # returns the projections for this team and bug list based on the
current
  # start date and duration in the project settings. See the doc under
  # self.best_projection for more infomation



                                   181
  def self.find_projections_by_team_and_bug_list(team, bug_list)
    saved_projections = Projection.find(:all, :conditions => {:team_id
=> team.id, :bug_list_id => bug_list.id } )

    projection_date_array = team.project.projection_dates
    ## these will be the projections we return.

    projections = Array.new

    projection_date_array.each do |required_projection_date|
      best_guess = Projection.best_projection(required_projection_date,
saved_projections)
      if best_guess.nil?
        empty_projection = Projection.new(:team_id => team.id,
:bug_list_id => bug_list.id, :date => required_projection_date,
:bug_count => -1)
        projections.push(empty_projection)
      else
        projections.push(best_guess)
      end

    end
    return projections

  end

  ##
  # find all the projections for a given bug list
  def self.find_projections_by_bug_list(bug_list)

    projection_date_array = bug_list.project.projection_dates

    project_projections = Array.new

    ## populate the project projections with new projections
    projection_date_array.each do |date|
       project_projections.push(Projection.new(:date => date, :bug_count
=> 0))
    end

    ## go through each team and add up their projections
    bug_list.project.teams.each do |team|


      team_projections =
Projection.find_projections_by_team_and_bug_list(team,bug_list)

         project_projections.each_with_index do |project_projection,
index|
           if team_projections[index].bug_count > 0
             ## we use -1 to show no projection, so we have to avoid
adding
          ## the -1s or our totalls end up smaller than they should be.
          project_projection.bug_count +=
team_projections[index].bug_count
        end
      end


                                      182
      end

      return project_projections


  end

  ##
  # Projections are set by teams based on weekly increments starting at
  # the project.projection_start_date and going out
project.projection_weeks.
  #
  # it's possible and even likely that a project administrator might
change
  # the project.projection_start_date or project.projection_weeks.
  #
  # if that happens then projections that have previously been entered
for a given
  # team and bug list might be set to dates that are no longer of
interest to the project
  #
  # But we don't want to throw out the existing projections because
some of them might be useful.
  # for example, if the projection start date is moved one day forward
from a sunday to a monday
  # the projections that were entered for sunday are still better than
nothing, so we should
  # carry them forward.
  #
  # this method does just that. in the case where there is not an exact
projection match
  # it finds the best one by looking at projections close to the
requred date.
  # it expexts an array of Projection Objects and a date for which it
will find a projection.
  def self.best_projection(requested_date, possible_projections)
     the_answer = nil
     possible_projections.each do |possible_projection|
         if ( possible_projection.date <= requested_date &&
possible_projection.date > (requested_date - 1.week)      )
             ## the count found is the bug count on or before
             the_answer = possible_projection
         else
           #logger.info("trying next projection")
         end
     end
     return the_answer
  end



end




                                   183
app/models/scheduler.rb
#   The scheduler is the brains of the controller. It decides which
#   projects should update and in which order.
#
#   If you are looking for the place to change the business logic for
#   site generation priority, this is the place.

require 'singleton'
require 'monitor'
class Scheduler < Monitor
  include Singleton

  ## This is the brains of the controller
  #   when a generator asks for work this is where
  #   the decision is made.
  #
  #   The only place that calls this code is
generations_controller.rb#create
  def next_project
    Project.find(:first, :order => 'id ASC')
  end

  ## temp work on a smarter conroller
  ##
  ## WARNING: this method is not synchronized, so if you're assigning
jobs
  ## based on this data, make sure
  ## you don't dole out the same site to two generatos
  def better_next_project(logger)

      ## first get all of the active projects
      active_projects = Project.find(:all, :conditions => {:active =>
true} )
      logger.info "Picking Projects Choices: " +
active_projects.length.to_s


        ## first add the weekly ones
        weekly_projects = Array.new
        daily_projects = Array.new
        constantly_projects = Array.new

        active_projects.each do |project|
          ## see if the project should generate
          if project.should_generate?
            case project.site_update_frequency.generator_name
            when 'weekly'
              ## the gerator is starting
              logger.info "Adding weekly project " + project.name
              weekly_projects.push(project)
            when 'daily'
              logger.info "Adding daily project " + project.name
              daily_projects.push(project)
            else



                                     184
              logger.info "Adding constantly project " + project.name
              constantly_projects.push(project)
            end
          end
        end

      ## give them a weekly project if you have one
      if !weekly_projects.empty?
        ##sort them and get going
        weekly_projects.sort! {|x,y| x.latest_complete_generation_time
<=> y.latest_complete_generation_time }
        logger.info "RETURNING WEEKLY PROJECT"
        return weekly_projects[0]
      end

      ## give them a daily project if you have one
      if !daily_projects.empty?
        ##sort them and get going
        daily_projects.sort! {|x,y| x.latest_complete_generation_time
<=> y.latest_complete_generation_time }
        logger.info "RETURNING DAILY PROJECT"
        return daily_projects[0]
      end

        ## and if there was no weekly or daily projects, do a constantly
one
      if !constantly_projects.empty?
        ##sort them and get going
        constantly_projects.each do |project|
          logger.info project.name + " :: " +
project.latest_complete_generation_time.to_s
        end
        constantly_projects.sort! {|x,y|
x.latest_complete_generation_time <=> y.latest_complete_generation_time
}
        logger.info "RETURNING CONSTANTLY PROJECT"
        return constantly_projects[0]
      end
      logger.info "RETURNING NIL PROJECT"

      ## this is really odd, somehow all the constantly projects are
generating inactive or deleted
      return nil

  end

  ## returns a saved generator with the next project that should be
generated
  def schedule_next(generator, logger)
    synchronize do
      project = Scheduler.instance.better_next_project(logger)
      if !project.nil?
        logger.info "Assigning Generation of project" + project.name
        generation = Generation.new
        generation.project = project
        generation.generator = generator
        generation.start_time = Time.now


                                     185
        generation.save

        ## make sure the latest info is at the site
        project.publish_site

        return generation
      end
      logger.info "Scheduler.instance.better_next_project found NO
PROJECTS"
      ## there was no project. return nil
      return nil
    end


  end




end




                                   186
app/models/scope_filter.rb
class ScopeFilter < ActiveRecord::Base
  has_many :bug_lists
end




                                   187
app/models/site_update_frequency.rb
class SiteUpdateFrequency < ActiveRecord::Base
  has_many :projects
end




                                      188
app/models/team.rb
class Team < ActiveRecord::Base
  belongs_to :project
  belongs_to :person
  has_many :owners
  has_many :team_members, :through => :owners, :source => :person

  #Teams are identified by their managers

  def count
    self.owners.count
  end

  def manager
    self.person
  end


  ##
  # this field is needed in order for the view to be able to
  # accept a username when creating a team
  # rails wants us to use fields from the model, but we really don't
  # want to tie this to the nested model at that point...
  # we need to run a bunch of logic before creating the team object
  def username
  end

  ##
  # this field is needed in order for the view to be able to
  # accept a username when creating a team
  # rails wants us to use fields from the model, but this field is
  # not part of the model
  def use_directory
  end


end




                                   189
config/environment.rb
# Be sure to restart your server when you modify this file

# Uncomment below to force Rails into production mode when
# you don't control web/app server and can't set it the proper way
# ENV['RAILS_ENV'] ||= 'production'

# Specifies gem version of Rails to use when vendor/rails is not
present
RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION

require 'yaml'

require 'global_helpers'




# Bootstrap the Rails environment, frameworks, and default
configuration
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  # Settings in config/environments/* take precedence over those
specified here.
  # Application configuration should go into files in
config/initializers
  # -- all .rb files in that directory are automatically loaded.
  # See Rails::Configuration for more options.

  # Skip frameworks you're not going to use (only works if using
vendor/rails).
  # To use Rails without a database, you must remove the Active Record
framework
  # config.frameworks -= [ :active_record, :active_resource,
:action_mailer ]

  # Only load the plugins named here, in the order given. By default,
all plugins
  # in vendor/plugins are loaded in alphabetical order.
  # :all can be used as a placeholder for all plugins not explicitly
named
  # config.plugins = [ :exception_notification, :ssl_requirement, :all
]

  # Add additional load paths for your own custom dirs
  # config.load_paths += %W( #{RAILS_ROOT}/extras )

  # Force all environments to use the same logger level
  # (by default production uses :info, the others :debug)
  # config.log_level = :debug

  # Your secret key for verifying cookie session data integrity.
  # If you change this key, all old sessions will become invalid!



                                   190
  # Make sure the secret is at least 30 characters and all random,
  # no regular words or you'll be exposed to dictionary attacks.
  config.action_controller.session = {
    :session_key => '_thesisenotify_session',
    :secret      =>
'511e226e764f8ad69dffcae9429bf9eacd2568ef188db454eb09b18bf8bad18577170a
1bb0455a5fe341b09032f27029c0aa0cdd0ef96d6bc09201363dcb50e3'
  }

  #   Use the database for sessions instead of the cookie-based default,
  #   which shouldn't be used to store highly confidential information
  #   (create the session table with 'rake db:sessions:create')
  #   config.action_controller.session_store = :active_record_store

  # Use SQL instead of Active Record's schema dumper when creating the
test database.
  # This is necessary if your schema can't be completely dumped by the
schema dumper,
  # like if you have constraints or database-specific column types
  # config.active_record.schema_format = :sql

  # Activate observers that should always be running
  # config.active_record.observers = :cacher, :garbage_collector

  # Make Active Record use UTC-base instead of local time
  # config.active_record.default_timezone = :utc



end


## APPLICATION CONSTANTS ##
# These are the applicaion constants. They work across all envirnments

EMAIL_DOMAIN = 'flubber.com'
SITES_DIRECTORY = '/Users/parussel/Documents/projects/thesis/sites/'
SITES_URL = '/sites/'




                                     191
config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resources :teams, :active_scaffold => true

  map.resources :site_update_frequencies

  map.resources :bug_ownership_changes

  map.resources :scope_filters

  map.resources :owners, :active_scaffold => true

  map.resources :bug_columns

  map.resources :projections

  map.resources :ownership_models

  map.resources :bug_lists, :active_scaffold => true

  map.resources :administrators, :active_scaffold => true

  map.resources :bug_tool_urls

  map.resources :people, :active_scaffold => true

  map.resources :site_update_frequencies

  map.resources :bug_list_downloads

  map.resources :generators, :collection => {:current_generation =>
:get}

  map.resources :generations

  map.resources :projects, :active_scaffold => true

  map.resource :logins

  map.resource :graphs

  # The priority is based upon order of creation: first created ->
highest priority.

  # Sample of regular route:
  #    map.connect 'products/:id', :controller => 'catalog', :action =>
'view'
  # Keep in mind you can assign values other than :controller and
:action

  # Sample of named route:
  #   map.purchase 'products/:id/purchase', :controller => 'catalog',
:action => 'purchase'
  # This route can be invoked with purchase_url(:id => product.id)



                                      192
  # Sample resource route (maps HTTP verbs to controller actions
automatically):
  #   map.resources :products

  # Sample resource route with options:
  #   map.resources :products, :member => { :short => :get, :toggle =>
:post }, :collection => { :sold => :get }

  # Sample resource route with sub-resources:
  #   map.resources :products, :has_many => [ :comments, :sales ],
:has_one => :seller

  # Sample resource route within a namespace:
  #   map.namespace :admin do |admin|
  #     # Directs /admin/products/* to Admin::ProductsController
(app/controllers/admin/products_controller.rb)
  #     admin.resources :products
  #   end

  # You can have the root of your site routed with map.root -- just
remember to delete public/index.html.
  # map.root :controller => "welcome"

  # See how all your routes lay out with "rake routes"

  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end




                                   193
db/migrate/002_create_projects.rb
class CreateProjects < ActiveRecord::Migration
  def self.up
    create_table :projects do |t|

      t.string :name, :null => false
      t.string :short_name, :null => false
      t.date :projection_start_date
      t.integer :site_update_frequency_id, :null => false
      t.integer :projection_weeks, :default => 0
      t.string :base_url
      t.integer :bug_tool_url_id
      t.string :table_column_order, :default =>
'1,7,18,4,5,6,10,12,13,14,2'
      t.boolean :active, :default => true

      t.timestamps
    end
  end

  def self.down
    drop_table :projects
  end
end




                                    194
db/migrate/003_create_generations.rb
class CreateGenerations < ActiveRecord::Migration
  def self.up
    create_table :generations do |t|
      t.integer :project_id, :null => false
      t.datetime :start_time
      t.datetime :end_time
      t.datetime :bug_list_download_start_time
      t.datetime :bug_list_download_end_time
      t.datetime :rendering_pages_start_time
      t.datetime :rendering_pages_end_time
      t.datetime :rendering_graphs_start_time
      t.datetime :rendering_graphs_end_time
      t.integer :generator_id, :null => false
      t.boolean :complete, :default => false

      t.timestamps
    end
  end

  def self.down
    drop_table :generations
  end
end




                                       195
db/migrate/004_create_generators.rb
class CreateGenerators < ActiveRecord::Migration
  def self.up
    create_table :generators do |t|
      t.string :name
      t.string :ip_address
      t.integer :current_generation_id
      t.boolean :blocked, :default => false

      t.timestamps
    end
  end

  def self.down
    drop_table :generators
  end
end




                                      196
db/migrate/005_create_bug_list_downloads.rb
class CreateBugListDownloads < ActiveRecord::Migration
  def self.up
    create_table :bug_list_downloads do |t|
      t.integer :bug_list_id
      t.integer :generation_id
      t.boolean :success
      t.integer :duration_seconds
      t.datetime :start_time
      t.datetime :end_time

      t.timestamps
    end
  end

  def self.down
    drop_table :bug_list_downloads
  end
end




                                     197
db/migrate/006_create_site_update_frequencies.rb
class CreateSiteUpdateFrequencies < ActiveRecord::Migration
  require 'active_record/fixtures'

  def self.up
    create_table :site_update_frequencies do |t|
      t.string :name
      t.string :generator_name
      t.string :description

      t.timestamps

    end

    directory = File.join(File.dirname(__FILE__),
"../constant_fixtures")
    Fixtures.create_fixtures(directory, "site_update_frequencies")
  end




  def self.down
    drop_table :site_update_frequencies
  end
end




                                     198
db/migrate/007_create_people.rb
class CreatePeople < ActiveRecord::Migration
  def self.up
    create_table :people do |t|
      t.string :username
      t.string :first_name
      t.string :last_name

      t.timestamps
    end
  end

  def self.down
    drop_table :people
  end
end




                                   199
db/migrate/008_create_bug_tool_urls.rb
class CreateBugToolUrls < ActiveRecord::Migration
  def self.up
    create_table :bug_tool_urls do |t|
      t.string :name
      t.string :description
      t.string :url

      t.timestamps
    end


    directory = File.join(File.dirname(__FILE__),
"../constant_fixtures")
    Fixtures.create_fixtures(directory, "bug_tool_urls")
  end

  def self.down
    drop_table :bug_tool_urls
  end
end




                                     200
db/migrate/010_create_administrators.rb
class CreateAdministrators < ActiveRecord::Migration
  def self.up
    create_table :administrators do |t|
      t.integer :project_id
      t.integer :person_id

      t.timestamps
    end
  end

  def self.down
    drop_table :administrators
  end
end




                                      201
db/migrate/011_create_bug_lists.rb
class CreateBugLists < ActiveRecord::Migration
  def self.up
    create_table :bug_lists do |t|
      t.integer :project_id
      t.integer :ownership_model_id
      t.string :name
      t.string :display_name
      t.string :display_name_color, :default => '000000'
      t.binary :description
      t.boolean :use_projections, :default => false
      t.boolean :use_ownership_change, :default => false
      t.boolean :is_verification_list, :default => false
      t.integer :scope_filter_id, :default => 1
      t.binary :expert_query
      t.string :table_column_order, :default => ''
      t.boolean :override_column_order, :default => false
      t.integer :position

      t.timestamps
    end
  end

  def self.down
    drop_table :bug_lists
  end
end




                                     202
db/migrate/012_create_ownership_models.rb
class CreateOwnershipModels < ActiveRecord::Migration
  def self.up
    create_table :ownership_models do |t|
      t.string :name
      t.string :legacy_name
      t.string :description


      t.timestamps
    end


    directory = File.join(File.dirname(__FILE__),
"../constant_fixtures")
    Fixtures.create_fixtures(directory, "ownership_models")
  end

  def self.down
    drop_table :ownership_models
  end
end




                                   203
db/migrate/013_create_projections.rb
class CreateProjections < ActiveRecord::Migration
  def self.up
    create_table :projections do |t|
      t.integer :bug_list_id
      t.integer :team_id
      t.date :date
      t.integer :bug_count

      t.timestamps
    end
  end

  def self.down
    drop_table :projections
  end
end




                                       204
db/migrate/014_create_bug_columns.rb
class CreateBugColumns < ActiveRecord::Migration
  def self.up
    create_table :bug_columns do |t|
      t.integer :application_id, :null => false
      t.integer :position
      t.string :field_name, :null => false
      t.string :display_name, :null => false
      t.string :qddts_query_field_name
      t.integer :minimum_width
      t.string :access_function
      t.string :description
      t.boolean :sortable
      t.boolean :dont_pass_field
      t.integer :sort_order
      t.string :sort_function

      t.timestamps

    end
    directory = File.join(File.dirname(__FILE__),
"../constant_fixtures")
    Fixtures.create_fixtures(directory, "bug_columns")
  end

  def self.down
    drop_table :bug_columns
  end
end




                                   205
db/migrate/015_create_owners.rb
class CreateOwners < ActiveRecord::Migration
  def self.up
    create_table :owners do |t|
      t.integer :team_id
      t.integer :person_id

      t.timestamps
    end
  end

  def self.down
    drop_table :owners
  end
end




                                   206
db/migrate/017_create_scope_filters.rb
class CreateScopeFilters < ActiveRecord::Migration
  def self.up
    create_table :scope_filters do |t|
      t.string :name
      t.string :generator_name
      t.string :description

      t.timestamps
    end


    directory = File.join(File.dirname(__FILE__),
"../constant_fixtures")
    Fixtures.create_fixtures(directory, "scope_filters")
  end

  def self.down
    drop_table :scope_filters
  end
end




                                         207
db/migrate/018_create_bug_ownership_changes.rb
class CreateBugOwnershipChanges < ActiveRecord::Migration
  def self.up
    create_table :bug_ownership_changes do |t|
      t.integer :bug_list_id
      t.string :bugid
      t.integer :new_owner_id
      t.integer :assigner_id

      t.timestamps
    end

  end

  def self.down
    drop_table :bug_ownership_changes
  end
end




                                   208
db/migrate/019_create_teams.rb
class CreateTeams < ActiveRecord::Migration
  def self.up
    create_table :teams do |t|
      t.integer :project_id
      t.integer :person_id

      t.timestamps
    end
  end

  def self.down
    drop_table :teams
  end
end




                                   209
lib/enotify_ldap.rb
# This module manages the interactions with the corporate LDAP server
# and hadles the logic for protecting against duplate entries for the
# same user in the aplication database

require 'ldap'

module EnotifyLdap



    SEARCH_BASE = 'ou=active,ou=employees,ou=people,o=harvard.com'

    FIELD_EMPLOYEE_IDS = 'directreports'

    FIELD_MANAGER_EMPLOYEE_NUMBER = 'manageruid'

    FIELD_EMPLOYEE_NUMBER = 'employeenumber'

    FIELD_EMPLOYEE_UID = 'uid'

    FIELD_EMPLOYEE_LAST_NAME = 'sn'

    FIELD_EMPLOYEE_PREFERED_FIRST_NAME = 'nickname'

    FIELD_EMPLOYEE_GIVEN_FIRST_NAME = 'givenName'

    REQUESTED_ATTRIBUTES = [FIELD_EMPLOYEE_PREFERED_FIRST_NAME,
FIELD_EMPLOYEE_GIVEN_FIRST_NAME, FIELD_EMPLOYEE_LAST_NAME,
                            FIELD_MANAGER_EMPLOYEE_NUMBER,
FIELD_EMPLOYEE_NUMBER, FIELD_EMPLOYEE_IDS, FIELD_EMPLOYEE_UID]

    ##
    # this is a test method it is not used by the application
    def get_full_person_from_ldap_by_username(username)

          conn = _setup_ldap()
          search_filter = FIELD_EMPLOYEE_UID + '=' + username
          person = Person.new

        conn.search(SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE,
search_filter) { |entry|
            puts entry.inspect
        }

    end

    ##
    # Checks LDAP for this username. Returns a new person object
    #   containing the username and users full name if the username
    #   is found in LDAP.
    #   If there is no match for the username in LDAP nil is returned.
    def get_person_from_ldap_by_username(username)




                                      210
           conn = _setup_ldap()
           search_filter = FIELD_EMPLOYEE_UID + '=' + username

           # get the person if they are in the DB
           person = Person.find_by_username(username)


        # get the person's info out of LDAP if it exists
        if (person.nil?)
           puts "starting ldap lookup"
            conn.search(SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE,
search_filter) { |entry|

              user_hash = entry.to_hash
              person = Person.new
              person.first_name =
user_hash[FIELD_EMPLOYEE_PREFERED_FIRST_NAME].to_s
              person.last_name =
user_hash[FIELD_EMPLOYEE_LAST_NAME].to_s
              person.username = username.to_s

              if person.first_name.empty?
                  #they don't have a nickname, so use their given name
                  person.first_name =
user_hash[FIELD_EMPLOYEE_GIVEN_FIRST_NAME].to_s
              end
            }
        end

           ## this will be nil if the username isn't in LDAP
           return person

    end

    ##
    # gets an array of person object for all of the people that
    # directly report to the username provided.
    def get_array_of_employees_from_ldap_by_manager_username(username)

           conn = _setup_ldap()
           search_filter = FIELD_EMPLOYEE_UID + '=' + username

           team_of_people = Array.new()

           direct_report_string = nil

           person = Person.new

        conn.search(SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE,
search_filter, REQUESTED_ATTRIBUTES) { |entry|
            #puts entry.inspect
            user_hash = entry.to_hash
            direct_report_string = user_hash[FIELD_EMPLOYEE_IDS].to_s
        }

           # if you don't find the manager, don't bother trying to find
the team


                                        211
            if direct_report_string.empty?
                return nil
            end

            ## get the employees for this manager
            if !direct_report_string.nil?
                direct_report_string.split(':').each do |number|
                    team_member = get_person_by_employee_number(number)
                    if !team_member.nil?
                      team_of_people.push(team_member)
                    end
                end
            end

            ## return nothing if we don't have a team
            if team_of_people.length == 0
                return nil
            end

            return team_of_people

      end

    ##
    # Takes an employee number and returns a person object for that
person.
    def get_person_by_employee_number(employeenumber)

        conn = _setup_ldap()
        search_filter = FIELD_EMPLOYEE_NUMBER + '=' + employeenumber
        person = nil
        conn.search(SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE,
search_filter, REQUESTED_ATTRIBUTES) { |entry|
            #puts entry.inspect
            user_hash = entry.to_hash

            ## this isn't the most efficient way to get the user since
we are basically
            ## double dipping into LDAP, but it eliminates complexity
in dealing with
            ## duplicates between the LDAP and Application DB.
            person =
get_person_from_ldap_by_username(user_hash[FIELD_EMPLOYEE_UID].to_s)

            }

            return person
      end

      def _setup_ldap()
          return LDAP::Conn.new(host='ldap.harvard.com')
      end
end




                                       212
lib/thesis_importer.rb
# This class provides all the mechanics for
# importing sites into the new enotify controller
# See script/import/project.rb for usage

require   'pathname'
require   'rexml/document'
require   'parsedate'
require   'csv'


class ThesisImporter

  @errors
  @error_count
  @TEST_MODE = false
  @logger = nil
  @bug_columns = nil
  @site_update_frequency_constantly_id
  @site_update_frequency_daily_id
  @site_update_frequency_weekly_id
  @bug_tool_url_CDETS_id
  @bug_tool_url_QDDTS_id

  def initialize
    @logger = Logger.new(STDOUT)
    @logger.level = Logger::INFO

    @error_count = 0
    @errors = Array.new
    ## load the bug column data
    @bug_columns =
YAML.load(File.read("test/fixtures/bug_columns.yml"))

    ## load the ids the for the update frequencies
    @site_update_frequency_constantly_id =
SiteUpdateFrequency.find_by_generator_name("constantly").id
    @site_update_frequency_daily_id =
SiteUpdateFrequency.find_by_generator_name("daily").id
    @site_update_frequency_weekly_id =
SiteUpdateFrequency.find_by_generator_name("weekly").id

     # load the bug tool URLs
     @bug_tool_url_CDETS_id =
BugToolUrl.find_by_url('http://cdets.harvard.com/apps/goto?identifier='
).id
     @bug_tool_url_QDDTS_id = BugToolUrl.find_by_url('http://wwwin-
metrics.harvard.com/cgi-bin/ddtsdisp.cgi?id=').id

  end




                                   213
  # parses and imputs the site_properties.xml file
  # TODO finish mapping to Project Object and save
  def load_project_data(site_home_directory)

    p1 = Pathname(site_home_directory)
    p1.children.each do |file|
      if file.directory? then

        @logger.info("Importing Site: " + file.to_s)



        ##   IMPORT SITE PROPERTIES   ##
        current_project = create_project_from_site_xml(file)
        save_object(current_project, current_project.name)




        ##    IMPORT SITE QUERIES    ##
        query_properties_file = file.to_s +
'/configuration/queries.xml'
        create_bug_lists_from_query_xml(query_properties_file,
current_project.id)

        ##    IMPORT PEOPLE AND SITE RELATIONSHIPS    ##
        loop_over_ownership_files(file.to_s + '/output/owner_lists/',
current_project.id)

        ## IMPORT PROJECTIONS   ##
        load_projections(file.to_s + '/output/projections/',
current_project.id)

        ## IMPORT OWNERSHIP CHANGES ##
        import_ownership_changes(file.to_s +
'/output/ownership_change/', current_project.id)

        ## create the site directories and publish the files
        current_project.publish_site

        @logger.info('Copying History Records to site directory')
        #@logger.info('cp -r ' + file.to_s + '/output/history/* ' +
current_project.site_history_directory)
        system('cp -r ' + file.to_s + '/output/history/* ' +
current_project.site_history_directory)

        @logger.info("Error count for import = " + @error_count.to_s )
        @errors.each do |error|
          @logger.info( error.to_s )
        end

        @logger.info("Completed Importing Site: " + file.to_s)
      end # if
    end # each do
  end #load_project_data


                                   214
  def save_object(object, name)
    if (!@TEST_MODE) then
       @logger.info( "Saving " + name + " to DB." )
       object.save

       if !object.errors.empty?
          @logger.info( "Error Saving " + name + " to DB.")
          object.errors.each{|attr,msg|
            @logger.info( "#{attr} - #{msg}" )
            @errors.push( "Error Saving " + name + " to DB: " +
"#{attr} - #{msg}" )
            @error_count = @error_count + 1
          }


       end

    end # TEST_MODE
  end


  ## imports a site_properties.xml file into the DB.
  def create_project_from_site_xml (site_directory)

    @logger.info("Creating Site Properties" + site_directory.to_s)

    project = Project.new

    @logger.info("New Project Created")


    doc = REXML::Document.new File.new( site_directory.to_s +
'/configuration/site_properties.xml' )

    ## short_name
    @logger.info( File.basename(site_directory.to_s, '.xml') )
    project.short_name = File.basename(site_directory.to_s, '.xml')


    ## name
    doc.elements.each("site-properties/property[@name='project_name']")
{ |element|
      @logger.info( "project_name: " + element.attributes["value"] )
      project.name = element.attributes["value"]
    }

    ## active
    doc.elements.each("site-properties/property[@name='site_active']")
{ |element|
      @logger.info( "site_active: " + element.attributes["value"] )
      project.active = element.attributes["value"]
    }




                                   215
    ## site_update_frequency
    doc.elements.each("site-
properties/property[@name='site_update_frequency']") { |element|
      @logger.info( "site_update_frequency: " +
element.attributes["value"] )

      if (element.attributes["value"] == 'constantly')
        project.site_update_frequency_id =
@site_update_frequency_constantly_id
      elsif (element.attributes["value"] == 'daily')
        project.site_update_frequency_id =
@site_update_frequency_daily_id
      elsif (element.attributes["value"] == 'weekly')
        project.site_update_frequency_id =
@site_update_frequency_weekly_id
      else
        @logger.error('Site Update Frequency failed to import for site
' + project.short_name )
      end


    }


    ## projection_weeks
    doc.elements.each("site-
properties/property[@name='number_of_projection_weeks']") { |element|
      @logger.info( "number_of_projection_weeks: " +
element.attributes["value"] )
      project.projection_weeks = element.attributes["value"]
    }




    ## bug_tool_url_id
    doc.elements.each("site-properties/property[@name='bug_tool_url']")
{ |element|
      @logger.info( "bug_tool_url: " + element.attributes["value"] )
      project.bug_tool_url_id =
site_property_get_but_tool_url(element.attributes["value"] )
    }


    ## table_column_order
    doc.elements.each("site-
properties/property[@name='bug_table_column_order']") { |element|
      @logger.info( "old bug_table_column_order: " +
element.attributes["value"] )

      new_bug_column_order =
site_property_get_bug_column_order(element.attributes["value"])
      @logger.info( "new bug_table_column_order: " +
new_bug_column_order )
      project.table_column_order = new_bug_column_order
    }


                                   216
    ## projection_start_date
    ## CONFIRM: Possible need to parse date?
    ## CONFIRM: yes, parsing is broken. dates show up as 0007 instead
of 2007
    doc.elements.each("site-
properties/property[@name='projection_start_date']") { |element|
      old_date = ParseDate.parsedate element.attributes["value"], true
      formated_date = old_date[1].to_s + '/' + old_date[2].to_s + '/' +
old_date[0].to_s

        @logger.info( "projection_start_date " + formated_date )
        project.projection_start_date = formated_date
    }


    ## save the project so we can get the project ID in order to use it
to map the administrator
    save_object(project, project.name)


      doc.elements.each("site-
properties/property[@name='administrator_email_address']") { |element|

      @logger.info( "administrator_email_address: " +
element.attributes["value"] )

        #get the email address and strip out the username
        admin_email = element.attributes["value"]

        ## only use the stuff before the @ in the email address
        admin_username = admin_email.split('@')[0]

        # lookup the person in the DB
        admin_person = Person.find_or_create_by_username(admin_username)



      # put the project index and the person index in the
administrators table
      project_admin = Administrator.new
      project_admin.project_id = project.id
      project_admin.person_id = admin_person.id

      project_admin.save
      @logger.info( "administrator_email_address: " +
element.attributes["value"] )
    }




    return project
  end # create_project_from_site_xml




                                     217
  ## import the bug lists for a site from query.xml
  def create_bug_lists_from_query_xml(file, project_id)



    doc = REXML::Document.new File.new( file )

    doc.elements.each("qddts-queries/query-def") { |element|

      list = BugList.new

      ## map this list to the current project
      list.project_id = project_id

      ## name
      @logger.info( "BugList name: " + element.attributes["name"] )
      list.name = element.attributes["name"]

      ## use projection
      #@logger.info( "use_projections: " +
element.attributes["use_projections"] )
      list.use_projections =
t_or_f(element.attributes['use_projections'])
      @logger.info( "use_projections: " + list.use_projections.to_s )

      ## use_ownership_change
      ##@logger.info( "use_ownership_change " +
element.attributes["use_ownership_change"] )
      list.use_ownership_change =
t_or_f(element.attributes['use_ownership_change'])
      @logger.info( "use_ownership_change: " +
list.use_ownership_change.to_s )

      ## is_verification_list
      ##@logger.info( "is_verification_list " +
element.attributes["is_verification_list"] )
      list.is_verification_list =
t_or_f(element.attributes['is_verification_list'])
      @logger.info( "is_verification_list: " +
list.is_verification_list.to_s )


      ## ownership_model_name
      @logger.info( "ownership_model_name (original): " +
element.attributes["ownership_model_name"] )
      ownership_model_id =
query_property_get_ownership_model(element.attributes["ownership_model_
name"])
      @logger.info( "ownership_model_id (new): " +
ownership_model_id.to_s )
      list.ownership_model_id = ownership_model_id

      ## scope_filter

      @logger.info( "scope_filter (old): " +
element.attributes["scope_filter"] )



                                   218
      scope_filter_id =
query_property_get_scope_filter(element.attributes["scope_filter"])
      @logger.info( "scope_filter (new): " + scope_filter_id.to_s )
      list.scope_filter_id = scope_filter_id


        ## display_name

      puts element.elements['display-name'].text
      @logger.info( "display_name " +
nil_fix(element.elements['display-name'].text ))
      list.display_name = nil_fix(element.elements['display-
name'].text)

      ## display_name_color
      @logger.info( "display_name_color " + element.elements['display-
name'].attributes['color'] )
      list.display_name_color = element.elements['display-
name'].attributes['color']

      ## description
      @logger.info( "description " +
nil_fix(element.elements['description'].text ))
      list.description = nil_fix(element.elements['description'].text)

      ## expert_query
      @logger.info( "expert_query " + nil_fix(element.elements['expert-
query'].text ))
      list.expert_query = nil_fix(element.elements['expert-
query'].text)

      ## table_column_order
      @logger.info( "table_column_order " +
element.elements['query_bug_table_column_order'].attributes['value'] )
      list.table_column_order =
site_property_get_bug_column_order(element.elements['query_bug_table_co
lumn_order'].attributes['value'])

      ## override_column_order
      @logger.info( "override_column_order " +
element.elements['query_bug_table_column_order'].attributes['override']
)
      list.override_column_order =
element.elements['query_bug_table_column_order'].attributes['override']

        ## save the bug list
        save_object(list, list.name)


    }
  end    #create_bug_lists_from_query_xml


   ## starting in the ownership direcotry this iterates
  ## over all of the ownership files
  def loop_over_ownership_files(ownership_directory, project_id)



                                       219
    Pathname(ownership_directory).children.each do |file|

        @logger.info( "parsing: " +file.to_s )

      # put the manager username in the DB
      # and get the manager_id
      @logger.info( "saving manager username: " +
File.basename(file.to_s, ".txt"))
      manager =
Person.find_or_create_by_username(File.basename(file.to_s, ".txt"))
      team = Team.new(:project_id => project_id, :person_id =>
manager.id)
      team.save

        CSV::Reader.parse(File.open(file.to_s, 'rb')) do |row|

          @logger.info( "username: " + row[0] )
          person = Person.find_or_create_by_username(row[0])
          @logger.info( "first: " + row[1] )
          person.first_name = row[1]
          @logger.info( "last: " + row[2] )
          person.last_name = row[2]

          #same this person
          @logger.info( "saving person: " + person.username )
          person.save

          ## Save the persons relationship to their manager
          ## and this project
          owner = Owner.new

          owner.team_id = team.id
          owner.person_id = person.id

          @logger.info( "saving person as owner: " + person.username )
          owner.save


        end

    end

  end


  def import_ownership_changes(ownership_change_directory, project_id)

    #sites\[sitename]\output\ownership_change\bug_list_#.txt
    @logger.info( "############### IMPORTING OWNERSHIP CHANGES
###############" )

    unless Pathname(ownership_change_directory).exist?
      ## if there was never an ownership change for this project
      ## the directory wont exist... deal with it by giving
      ## up and getting a drink



                                        220
      @logger.info( "There is no ownership chance directory @ " +
ownership_change_directory.to_s + " moving on.")
      return
    end

    Pathname(ownership_change_directory).children.each do |file|
      ## you have an ownership file for a bug list

      ## get the bug list name out of the file name
      @logger.info( "finding buglist: " + File.basename(file.to_s,
".txt"))
      bug_list_name = File.basename(file.to_s, ".txt")
      bug_list = BugList.find(:first, :conditions => { :name =>
bug_list_name, :project_id => project_id } )

      #bug_list = BugList.find_by_name(File.basename(file.to_s,
".txt"))

      ##  it is possible that someone created a bug list, made
      ##  ownership changes, and then deleted the bug list
      ##  this would have left ownership_changes in the system
      ##  we have to ignore those changes or we won't be able to
      ##  tie them to a bug list
      if  bug_list.nil?
        @logger.info( "Skipping import of ownship change for
nonexistant bug list: " + File.basename(file.to_s, ".txt"))
        next
      end



      ## loop over each line of the bug list ownerhsip change file
      file = File.new(file, "r")
      while (line = file.gets)
        @logger.info( "parsing: " + line )

           ownership_change = BugOwnershipChange.new

           ownership_change.bug_list_id = bug_list.id

           ownership_array = line.split(':')

        ## the format of the ownership change file is:
        #   bugID:newowner:timestamp:admin_username
        ownership_change.bugid = ownership_array[0]
        ownership_change.new_owner_id =
Person.find_or_create_by_username(ownership_array[1]).id
        ## we skip the time stamp in ownership_array[2] because we
don't care
        ownership_change.assigner_id =
Person.find_or_create_by_username(ownership_array[3]).id

        # and we're done with this line. save it!
        save_object(ownership_change, line)
      end
      file.close



                                      221
    end


  end




  ## go through the projections directory and put all of the
projections
  ## into the DB
  def load_projections(projections_directory, project_id)

    @logger.info( "###############     PARSING PROJECTIONS
###############" )

    Pathname(projections_directory).children.each do |team_directory|
      next if !team_directory.directory? ## don't parse non
directories

        ## say which directory we're parsing
        @logger.info('Parsing: ' + File.basename( team_directory.to_s))

      ## get the manager username from the team_folder
      manager_username =
File.basename(team_directory.to_s).split("_")[0]

        @logger.info('Manager Username: "' + manager_username + '"')



        manager = Person.find_by_username(manager_username)

      ## there might be old team directories where the managers are no
longer part of the project
      # just punt on these
      next if manager.nil?

      team = Team.find(:first, :conditions => { :person_id =>
manager.id, :project_id => project_id } )


        ## manager is nice, but we actually need to Owner ID name
        ## for the manager in this project

        team_directory.children.each do |projection_file|

        ## get the bug list name
        bug_list_name = File.basename(projection_file.to_s, ".txt")
        bug_list = BugList.find(:first, :conditions => { :name =>
bug_list_name, :project_id => project_id } )

        next if bug_list.nil? ## there might be old bug lists that
were deleted
        ## where the projections have been left around




                                     222
           CSV::Reader.parse(File.open(projection_file.to_s, 'rb')) do
|row|
             projection = Projection.new

             ## Add the known info about the bug list and the owner.
             projection.bug_list_id = bug_list.id

             projection.team_id = team.id

             @logger.info( "date: " + Time.at(row[0].to_i).to_s )
             projection.date = Time.at(row[0].to_i)
             @logger.info( "bug_count: " + row[1] )
             projection.bug_count = row[1]

             save_object(projection, 'projection')

           end ## edn parsing this projection file

         end ## end looping through projection files in a team directory.

        end ## looping through team projection directories

   end ## load_projections




  ## takes an objec and replaces it with an empty string if the objec
is nil
  def nil_fix(value)
    if (nil == value)
       return ""
    else
       value
    end
  end



  ## returns the applicaion_id given
  ## the legacy name for that ownership model
  def query_property_get_ownership_model(old_name)
     OwnershipModel.find_by_legacy_name(old_name.to_s).id
 end

  ## returns the applicaion_id given
  ## the legacy name for a scope filter
  def query_property_get_scope_filter(old_name)
    ScopeFilter.find_by_generator_name(old_name.to_s).id
  end

  ## takes a URL and figures out the application_id for
  ## that URL in the new shcema
  def site_property_get_but_tool_url(legacy_url)



                                      223
    if (
'http://cdets.harvard.com/apps/goto?identifier='.eql?(legacy_url) )
      # CDETS SI URL found
      return @bug_tool_url_CDETS_id
    elsif   ( 'http://wwwin-metrics.harvard.com/cgi-
bin/ddtsdisp.cgi?id='.eql?(legacy_url) )
      # QDDTS URL found
      return @bug_tool_url_QDDTS_id
    else
      # unrecognized or empty legacy_url. use the default CDETS
      return @bug_tool_url_CDETS_id
    end

  end


  ## this method takes a legacy column order string like
  ##       'Bug ID,OWNER,Due-date,Severity,DE-Priority,Age,State,DE-
manager,Found,Headline'
  ## and turns masps each value to it's application_id as
  ## defined in the bug_columns fixture.
  ## the result is a string of comma seperated application_ids
(integers)
  def site_property_get_bug_column_order(legacy_order)

    legacy_order_array = legacy_order.split(',')

    ## this will hold the new column order
    new_bug_column_order = Array.new

    unless !legacy_order_array.nil?
        # lookup the id for each column name
        # this will impload if there is a rogue column name in site
properties
        # if it does you'll see the error
        # thesis_importer.rb:(LINE_NUMBER): undefined method `[]' for
nil:NilClass (NoMethodError)
        # but that should not happen since it's a enotify controlled
config file.
        legacy_order_array.each do |element|
           new_id = @bug_columns[element.to_s]["application_id"]
           new_bug_column_order.push(new_id)
        end
    end
    return new_bug_column_order.join(',')
  end


  def site_property_load_default_site_frequency ()

  end

  def t_or_f(value)
    if !value.nil?
        if value.eql?('false')
            return true
        elsif value.eql?('true')


                                   224
              return true
        end
    end
    return true

  end
end




                            225
lib/user_info.rb
# this modeule provides the ability to pass the current user to the
model
# the controller needs this capability because the project object
# needs to checnk the current user to make a number of permission
related
# decisions.
module UserInfo

  def current_user
    Thread.current[:user]
  end

  def self.current_user=(user)
    Thread.current[:user] = user
  end

end




                                   226
script/import/project.rb
## to run this script as part of the rails project
## type the following into a rails shell
##


#####################################################
## When running stand alone with the rails env
## uncomment this
#####################################################
## script/runner script/import/project.rb
#require "Pathname"
#require "rexml/document"
#require "Logger"

#logger = Logger.new(STDOUT)
#logger.level = Logger::INFO

#    SITE_HOME_DIRECTORY = 'C:\home\parussel\dev\education\thesis\sites'
#    OWNER_SUB_DIRECTORY = 'output/owner_lists'

########################################################




  SITE_HOME_DIRECTORY =
'/Users/parussel/Documents/projects/thesis/sites_to_import'
  puts 'SITE_HOME_DIRECTORY = ' + SITE_HOME_DIRECTORY

    magic_importer = ThesisImporter.new


    magic_importer.load_project_data(    SITE_HOME_DIRECTORY )




                                        227
script/thesisdoc/generate_doc.rb
#   This script will traverses my thesis project code tree and
#   formats the code for inclusion in the thesis document
#   per the requirements of the thesis guide.
#
#   The thesis guide suggested that copying and forating the
#   code for the thesis document would be time comsuming, which I
#   took as a suggestion that copying the code was meant to be
#   a manual process.
#
#   One the cusp of completing my masters in infomation technology
#   with a focus on software technology, I can't help but
#   think this is a test. My guess is that if a student manually
#   coppies and formats 113 pages of code in a MS word document,
#   they are not allowed to graduate. Thus I wrote this script,
#   and included it in my thesis project.
#
#   While it's not a pretty script, it did format the code with
#   no additional manual effort required on my part.
#
#   And that is a beautiful thing.

require 'pathname'

@doc = ''

start_directory = Pathname(".")


FILE_TYPES = Hash.new(0)

##
# These are the directories to ignore when outputing the code
EXCLUDE_DIR = [ "vendor", "tmp", "test", "script",
                "config", "doc", "generator", "lib/tasks",
                "log", "public", "app/helpers",
                "app/views/bug_columns",
                "app/views/bug_list_downloads",
                "app/views/bug_lists",
                "app/views/bug_ownership_changes",
                "app/views/bug_tool_urls",
                "app/views/generations",
                "app/views/generators",
                "app/views/layouts",
                "app/views/owners",
                "app/views/ownership_models",
                "app/views/scope_filters",
                "app/views/site_update_frequencies",
                ]

##
# These are the files that should be excluded even though they are
# in included directories
EXCLUDE_FILE = [ "p",



                                     228
                "db/development_structure.sql",
                "db/schema.rb",
                "lib/global_helpers.rb",
                "Rakefile",
                "README",
                "t",
                "l",
                ".project",
                ".loadpath",
                ".DS_Store",
                "app/.DS_Store",
                "tp",
                "output.html",
                "app/controllers/bug_colums_controller.rb",
                "app/controllers/bug_list_downloads_controller.rb",
                "app/controllers/bug_ownership_changes_controller.rb",
                "app/controllers/bug_tool_urls_controller.rb",
                "app/controllers/generators_controller.rb",
                "app/controllers/ownership_models_controller.rb",
                "app/controllers/pauls_controller.rb",
                "app/controllers/scope_filters_controller.rb",

"app/controllers/site_update_frequencies_controller.rb",
                "app/views/adminstrators/index.html.erb",
                "app/views/people/index.html.erb",
                "app/views/people/new.html.erb",
                "app/views/projections/new.html.erb",
                "app/views/projections/index.html.erb",
                "app/views/projects/new.html.erb",
                "app/views/teams/edit.html.erb",
                "app/views/teams/index.html.erb"
                ]


# These are files to include that are under excluded
#   directories, but should still should be included.
ADD_FILE = [ "script/import/project.rb",
             "script/thesisdoc/generate_doc.rb",
             "config/environment.rb",
             "config/routes.rb",
             "config/database.yml",
             "generator/Enotify/Generator/ControllerCommunicator.pm",
             "public/stylesheets/custom.css",
             "app/helpers/bug_columns_helper.rb",
             "app/helpers/bug_lists_helper.rb",
             "app/helpers/owners_helper.rb",
             "app/helpers/projects_helper.rb",
             "app/helpers/teams_helper.rb",
             "app/views/bug_lists/for_project.html.erb",
             "app/views/layouts/admin_standard.html.erb",
             "app/views/owners/new.html.erb"
             ]


FILE_TYPE_MAPPING = { ".erb" => "Embedded Ruby (.erb)",
                      ".rb" => "Ruby (.rb)",
                      ".css" => "Cascading Style Sheet (.css)",


                                   229
                         ".yml" => "YAML Ain't Markup Language (.yml)",
                         ".pm" => "Perl (.pm)"
                         }


FILES_TO_RENDER = Array.new



def generate_file_list(directory)
  directory.children.each do |file|
    if file.directory? then
      unless is_directory_excluded?(file)
         generate_file_list(file)
      end
    else
        unless is_file_excluded?(file)
          FILES_TO_RENDER.push(file.to_s)
        end
    end
  end
end

def add_files()
  ADD_FILE.each { |filepath|
    FILES_TO_RENDER.push(filepath)
  }
end



def is_directory_excluded?(directory)
  if (EXCLUDE_DIR.include?(directory.to_s) ||
directory.to_s.include?('.svn') )
    puts "excluding " + directory.to_s
    return true
  else
    return false
  end
end

def is_file_excluded?(file)
  if (EXCLUDE_FILE.include?(file.to_s) || file.to_s.include?('.svn') )
    puts "excluding " + file.to_s
    return true
  else
    return false
  end
end


def write_to_doc(text)
  @doc += text
end




                                      230
def render_file_type(extension)
  puts "rendering #{extension}"
  write_to_doc "<h2>" + FILE_TYPE_MAPPING[extension] + "</h2>"

  FILES_TO_RENDER.each { |filepath|
    file = Pathname(filepath)
      if file.extname.eql?(extension)
        render_file(file)
      end
  }
end


def render_file(file)

    write_to_doc "<h3>" + file.to_s + "</h3>"
    write_to_doc "<pre>" + file.read.to_s.gsub(/\</, '<') + "</pre>"

end

## traverse the project directory and get all non excluded files
generate_file_list(start_directory)

## get any included files from excluded directories
add_files()

## Sort all the files alphabetically
FILES_TO_RENDER.sort!

## count up how many of each file type we have
FILES_TO_RENDER.each { |filepath|
  file = Pathname(filepath)

    FILE_TYPES[file.extname] = FILE_TYPES[file.extname] + 1
}


SORTED_FILE_TYPES = FILE_TYPES.keys.sort


puts FILES_TO_RENDER.join("\n")

FILE_TYPES.each_pair {|key, value| puts "boo #{key} has #{value}" }

SORTED_FILE_TYPES.each { |extension|
  render_file_type(extension)
}

# Obviously I'm hiding the names of something the code,
# but if you're reading this in my thesis document it will
# look like I replaced harvard with harvard.
#
# As it is of no signifigance this is one piece of the
# project that can be safely lost to history.
@doc.gsub!(/harvard/, 'harvard')
@doc.gsub!(/Harvard/, 'Harvard')
@doc.gsub!(/flubber/, 'flubber')


                                     231
@doc.gsub!(/FLUBBER/, 'FLUBBER')


my_file = File.new("../output.html", "w+")
my_file.puts @doc




                                   232
                      YAML Ain't Markup Language (.yml)



config/database.yml
# MySQL. Versions 4.1 and 5.0 are recommended.
#
# Install the MySQL driver:
#   gem install mysql
# On Mac OS X:
#   sudo gem install mysql -- --with-mysql-dir=/usr/local/mysql
# On Mac OS X Leopard:
#   sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-
config=/usr/local/mysql/bin/mysql_config
#       This sets the ARCHFLAGS environment variable to your native
architecture
# On Windows:
#   gem install mysql
#       Choose the win32 build.
#       Install MySQL and put its /bin directory on your path.
#   # adapter: sqlite3
  # database: db/production.db
# And be sure to use new-style password hashing:
#   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
development:
  adapter: mysql
  encoding: utf8
  database: thesisedev_development
  username: thesisdev
  password: thesisdevpw
  host: 127.0.0.1
  port: 3306

# Warning: The database defined as 'test' will be erased and
# re-generated from your development database when you run 'rake'.
# Do not set this db to the same as development or production.
# adapter: sqlite3
# database: db/test.db
test:
  adapter: mysql
  encoding: utf8
  database: thesisedev_test
  username: thesisdev
  password: thesisdevpw
  host: 127.0.0.1
  port: 3306

# this is the production db
production:
  adapter: mysql
  encoding: utf8
  database: thesisedev_production
  username: thesisdev
  password: thesisdevpw



                                     233
host: 127.0.0.1
port: 3306




                  234
db/constant_fixtures/bug_columns.yml
# Defines the bug columns. This is application data needed by the
generators.
# it is used to map the fields returned from the internal bug system to
# fields that people want to see.


Bug ID:
    application_id: 1
    field_name: Bug ID
    display_name: Bug ID
    qddts_query_field_name: Identifier
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_bug_id_link
    description: The Unique Identifier for the Bug
    sortable: 1
    sort_order: 1
    sort_function: get_field

Headline:
    application_id: 2
    field_name: Headline
    display_name: Headline
    qddts_query_field_name: Headline
    minimum_width: 200
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: The Headline for the Bug
    sortable: 0

Due-date:
    application_id: 3
    field_name: Due-date
    display_name: Due Date
    qddts_query_field_name: Due-date
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_date_field
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_field

Est-fix-date:
    application_id: 4
    field_name: Est-fix-date
    display_name: Est fix date
    qddts_query_field_name: Est-fix-date
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_date_field
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_field

Severity:



                                       235
    application_id: 5
    field_name: Severity
    display_name: Sev
    qddts_query_field_name: Severity
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_field

DE-Priority:
    application_id: 6
    field_name: DE-Priority
    display_name: DE-P
    qddts_query_field_name: DE-priority
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_field

OWNER:
    application_id: 7
    field_name: OWNER
    display_name: Owner
    qddts_query_field_name: IGNORE
    minimum_width: 1
    access_function:
FLUBBER::Utils::DDTS::Read::Bug::get_owner_username
    dont_pass_field: true
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_owner_username

Submitter:
    application_id: 8
    field_name: Submitter
    display_name: Submitter
    qddts_query_field_name: Submitter-id
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: The Submitter of the Bug
    sortable: 1
    sort_order: 1
    sort_function: get_field

Resolver-id:
    application_id: 9
    field_name: Resolver-id
    display_name: Resolver
    qddts_query_field_name: Resolver-id
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: The Resolver of the Bug


                                   236
       sortable: 1
       sort_order: 1
       sort_function: get_field

Age:
       application_id: 10
       field_name: Age
       display_name: Age
       qddts_query_field_name: IGNORE
       minimum_width: 1
       access_function: FLUBBER::Utils::DDTS::Read::Bug::get_age
       dont_pass_field: true
       description:
       sortable: 1
       sort_order: 2
       sort_function: get_age

Age-ascending:
    application_id: 11
    field_name: Age-ascending
    display_name: Age
    qddts_query_field_name: IGNORE
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_age
    dont_pass_field: true
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_age

State:
    application_id: 12
    field_name: State
    display_name: State
    qddts_query_field_name: Status
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_field

DE-manager:
    application_id: 13
    field_name: DE-manager
    display_name: DE-M
    qddts_query_field_name: DE-manager
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description:
    sortable: 1
    sort_order: 1
    sort_function: get_field

Found:
    application_id: 14
    field_name: Found


                                      237
    display_name: Found
    qddts_query_field_name: Found
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: Versions where the bug was found
    sortable: 1
    sort_order: 1
    sort_function: get_field


To be fixed in:
    application_id: 15
    field_name: To be fixed in
    display_name: To be fixed
    qddts_query_field_name: To-be-fixed
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1
    sort_function: get_field

Apply-to:
    application_id: 16
    field_name: Apply-to
    display_name: Apply-to
    qddts_query_field_name: Apply-to
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1
    sort_function: get_field

Priority:
    application_id: 17
    field_name: Priority
    display_name: Cust Priority
    qddts_query_field_name: Priority
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: The Customer Priority of the bug
    sortable: 1
    sort_order: 1
    sort_function: get_field

Component:
    application_id: 18
    field_name: Component
    display_name: Component
    qddts_query_field_name: Component
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: The component in which this bug is found
    sortable: 1
    sort_order: 1
    sort_function: get_field

EDA_LINK:


                                   238
    application_id: 19
    field_name: EDA_LINK
    display_name: EDA link
    qddts_query_field_name: IGNORE
    minimum_width: 1
    access_function:
FLUBBER::Utils::DDTS::Read::Bug::get_eda_link_for_bug
    dont_pass_field: true
    description: Link to the VTG EDA tool for a given bug
    sortable: 0

T-Daddy_LINK:
    application_id: 20
    field_name: T-Daddy_LINK
    display_name: T-Daddy link
    qddts_query_field_name: IGNORE
    minimum_width: 1
    access_function:
FLUBBER::Utils::DDTS::Read::Bug::get_tdaddy_link_for_bug
    dont_pass_field: true
    description: Link to the user defined field edit page for a given
bug
    sortable: 0

Attribute:
    application_id: 21
    field_name: Attribute
    display_name: Attribute
    qddts_query_field_name: Attribute
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1
    sort_function: get_field

Product:
    application_id: 22
    field_name: Product
    display_name: Product
    qddts_query_field_name: Product
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    description: The product in which this bug is found
    sortable: 1
    sort_order: 1
    sort_function: get_field


Engineer assigned:
    application_id: 23
    field_name: Engineer assigned
    display_name: Engineer
    qddts_query_field_name: Engineer
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1


                                   239
    sort_function: get_field



Integrated-releases:
    application_id: 24
    field_name: Integrated-releases
    display_name: Integrated-releases
    qddts_query_field_name: Integrated-releases
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1
    sort_function: get_field

Impact:
    application_id: 25
    field_name: Impact
    display_name: Impact
    qddts_query_field_name: Impact
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1
    sort_function: get_field

Impact-level:
    application_id: 26
    field_name: Impact-level
    display_name: Impact-level
    qddts_query_field_name: Impact-level
    minimum_width: 1
    access_function: FLUBBER::Utils::DDTS::Read::Bug::get_field
    sortable: 1
    sort_order: 1
    sort_function: get_field




                                   240
db/constant_fixtures/bug_ownership_changes.yml
# Read about fixtures at
http://ar.rubyonrails.org/classes/Fixtures.html




                                    241
db/constant_fixtures/bug_tool_urls.yml
# Read about fixtures at
http://ar.rubyonrails.org/classes/Fixtures.html

cdets-si:
  name: CDETS SI Mode
  url: http://cdets.harvard.com/apps/goto?identifier=
  description: CDETS read/write bug web applicaion (works in all
browsers)

qddts:
  name: QDDTS
  url: http://wwwin-metrics.harvard.com/cgi-bin/ddtsdisp.cgi?id=
  description: QDDTS read-only bug web page

##cdets-hi:
## application_id: 3
## name: CDETS HI Mode
## url: http://cdets.harvard.com/apps/goto?identifier=
## description: CDETS read/write bug web applicaion (IE Only)




                                         242
db/constant_fixtures/ownership_models.yml
# Read about fixtures at
http://ar.rubyonrails.org/classes/Fixtures.html

## these ownership models were created by copying data from two places
in
## the legacy system.
##        1) The online help documetation
##        2) legacy\lib\FLUBBER\BugSorter\Constants.pm


DEFAULT:
  name: State Based - default
  legacy_name: DEFAULT
  description: see the online help


STATE_BASED_MODEL_TWO:
  name: State Based - I state owner = Engineer then De-Manager
  legacy_name: STATE_BASED_MODEL_TWO
  description: see the online help


FIELD_BASED_DEMANAGER:
  name: Owner is De-Manager
  legacy_name: FIELD_BASED_DEMANAGER
  description: see the online help



FIELD_BASED_ENGINEER:
  name: Owner = Engineer then De-Manager
  legacy_name: FIELD_BASED_ENGINEER
  description: see the online help


EDA_MODEL:
  name: Early Defect Analysis (EDA)
  legacy_name: EDA_MODEL
  description: see the online help




                                      243
db/constant_fixtures/scope_filters.yml
# Read about fixtures at
http://ar.rubyonrails.org/classes/Fixtures.html

## these values were copped from the legacy
## constants found in legacy\lib\FLUBBER\BugSorter\Constants.pm

individual:
    name: individual
    generator_name: individual
    description: Show on all pages - default - individual, team, &
project (default)

team:
    name: team
    generator_name: team
    description: Show on team & project only

project:
    name: project
    generator_name: project
    description: Show on project only

disable:
    name: disable
    generator_name: disable
    description: Disable on all pages




                                         244
db/constant_fixtures/site_update_frequencies.yml
# Read about fixtures at
http://ar.rubyonrails.org/classes/Fixtures.html

constantly:
  name: Constantly
  generator_name: constantly
  description: Update the site as frequently as possible

daily:
  name: Daily
  generator_name: daily
  description: Update the site once a day

weekly:
  name: Weekly
  generator_name: weekly
  description: Update the site once a week




                                      245

				
DOCUMENT INFO
Shared By:
Categories:
Tags:
Stats:
views:11
posted:6/14/2012
language:English
pages:262