Banco Horas

Integrantes:

  • Douglas Bolkliang Ang Cunha
  • Fábio Eigi Imada
  • Rafael Rodrigues Siqueira
  • Rodrigo Simões de Alemida

Ano/sem: 2008/2o.
data da apresentação: cagamos o pau e não apresentamos

Apresentação

A idéia do projeto (automação de um Banco de Horas) foi criar um projeto "simples" em Ruby on Rails que permitisse ao mesmo tempo mostrar em prática as idéias (sobretudo padrões de projeto) vistas no curso mas ao mesmo tempo gerar algo útil.

Motivação

Tanto a ITA Júnior como a CV, duas iniciativas do H8, usam banco de horas (com propósitos diferentes, mas de maneira semelhante). Para a CV, o Banco de horas é um meio de verificar se todos os participantes estão se dedicando de maneira minimamente uniforme, para não haverem "injustiças" mais tarde, na hora de dividir o dinheiro. Já a ITA Júnior usa o banco de horas para acompanhamento dos membros (isto é, o diretor de cada departamento, ao olhar para a planilha preenchendo semanalmente, irá verificar não só se o membro está trabalhando pouco, mas também se ele está trabalhando nas atividades adequadas e sabendo dividir seu tempo - além disso é uma forma do diretor saber o quê cada membro está fazendo).

Em ambos os casos (CV e ITA Júnior), cada membro semanalmente tem que preencher uma tabela contendo, para cada atividade, uma descrição da mesma e o tempo investido. No caso da CV, era obrigatório preencher a qual depto tal atividade estava relacionada, pois os departamentos são flexíveis e muitas vezes membros de um depto trabalham no contexto de outro. Na ITA Júnior, era obrigatório colocar o dia da semana em que a atividade foi realizada, assim tem-se um indicador adicional mostrando como os membros dividem suas atividades ao longo da semana.

Do lado administrativo (na ITA Júnior, comandado pelo diretor executivo, na CV, pelo diretor de RH - sendo que todos os outros diretores tem acesso às planilhas de todo mundo), os administradores tem que poder ver as planilhas de qualquer membro, relativa a qualquer semana, e fora isso deveriam ser gerados relatórios mostrando, para determinados intervalos de tempo, um sumário mostrando quanto cada membro, departamento e empresa trabalhou (em média).

Até então essas atividades vinham sendo (em ambos os casos) gerenciadas via Excel, e cada usuário deveria enviar uma planilha de Excel por semana. Apesar desse método ser minimamente eficiente e consumir apenas algumas horas por semana, envolve a realização de tarefas repetitivas por parte dos administradores, o que por si só já seria um bom motivo para realizar o projeto. Entretanto, pior do que isso é que o preenchimento via Excel não impede que membros preencham os campos erradamente. Só o campo duração, por exemplo, é um problema: para escrever 5 horas e meia, pelo menos as quatro opções a seguir poderiam aparecer: 5.5; 5,5; 5h30; 5.30 - sendo que apenas uma delas seria reconhecida como número, de maneira correta, pelo Excel, as outras iam dar dor de cabeça na hora de compilar os dados. E, quando o administrador tem que compilar 30-50 planilhas por semana, um problema desse tipo pode passar despercebido com o Excel dando 0 horas para aquela entrada enquanto o correto seria 5.5. Além disso, estando todos os dados num banco de dados corretamente modelado, fica muito fácil a geração de relatórios e a possibilidade de se fazerem consultas e gerarem-se estatísticas mais complexas.

Screenshoots

Porque eles estão aqui?

É melhor ver o resultado final e depois tentar entender o porquê das decisões tomadas abaixo

Tela de Login

lsp1.jpg

Página Inicial

lsp2.jpg

Cadastro de Horas Trabalhadas

lsp3.jpg

"Meu Histórico"

lsp4.jpg

Listagem dos Membros (p/ Admins)

lsp5.jpg

Histórico Membro a Membro

lsp6.jpg

Cadastro do RH - Aplicativo Separada

lsp7.jpg

Etapas do desenvolvimento

Levantamento dos Requisitos

Nessa etapa, o grupo se reuniu para definir como seria o controle de horas, que telas ele teria, como ele seria usado, e como resolver as pequenas diferenças entre CV e ITA Júnior (nesse momento, decidimos colocar todas as colunas que eram particulares a cada iniciativa, e colocar uma coluna adicional onde o usuário deveria classificar o tipo de atividade em Reunião/Evento/Outras. Isso porque o trabalho em reunião não traz resultados diretamente e é o mínimo que cada membro tem que fazer, o que ele trabalha além disso é que vai trazer frutos. Já o trabalho em eventos pode acabar gerando muitas horas trabalhadas (ex: uma reunião em SP facilmente toma 6 horas só com o tempo de ir e voltar de ônibus - óbvio que o membro realmente estava a serviço da sua iniciativa nesse tempo, mas uma categorização aqui tornaria mais imediato perceber certos picos inexplicáveis de trabalho em algumas planilhas)).

A única coisa que produzimos aqui foi um documento no bloco de notas com os principais tópicos discutidos e acordados - isto é, não passamos por nenhum processo formal de levantar requisitos

Modelagem inicial do banco de dados

Uma vez definidas as ferramentas de trabalho, a próxima etapa consistiu na modelagem do banco de dados

Sabiamos que o esquema poderia (e de fato alterou-se) mudar mais tarde, mas o esquelo que geramos nesse dia seria o ponto de partida para o início do desenvolvimento e o grosso dele dificilmente se alteraria. De fato, só houve uma alteração mais profunda, que será explicada mais adiante.

Utilizamos o programa DBDesigner para esse modelamento inicial, e não fizemos manutenção desse documento depois que as migrações do Rails foram geradas, até porque essas últimas, junto com os models, produzem documentação suficiente para se manter controle de um banco de dados pequeno (menos de 10 tabelas)

Update: Modelo atual

modelo-bd.png

Codificação

A codificação do programa seguiu, de maneira aproximada, a seguinte seqüência:

  • Geração das entidades e migrações (tradução do modelo do BD para Rails)
  • Criação do sistema de login
  • Inserção manual de alguns usuários
  • Criação da tela para inserção da planilha com as horas de trabalho para cada semana
  • Testes dessa tela
  • Permitir editar as planilhas
  • Importação dos dados da CV (aqui usamos a biblioteca WIN32OLE, do Ruby, e fomos beneficiados pelo fato que os dados da CV já se encontravam pré-compilados num padrão muito bem definido, o processo todo - incluindo a geração dos scrips - levou poucas horas)
  • Criação da página inicial (que deveria mostrar gráficos comparando o desempenho de cada membro com seu depto e a empresa)
  • Ajustes finos nos gráficos
  • Separação do gerenciamento de usuários para um aplicativo separado (discutido mais a frente)
  • Testes desse novo sistema e da integração com o banco de horas
  • Otimizações de performance na geração dos dados para os gráficos
  • Parte administrativa básica - permitir acesso a qualquer planilha de qualquer usuário

Ainda continuaremos a aperfeiçoar o programa e a adicionar mais recursos, assim como desenvolver mais testes unitários (apenas um ou outro componente pode, neste momento, ser testado de maneira mais automática)

Desde o início o desenvolvimento se deu com código versionado, o qual ficou hospedado no Google Code. O desenvolvimento foi feito inteiramente em Rails e no Netbeans

Feita essa descrição inicial, queríamos focar em duas coisas: padrões de projeto e problemas/soluções importantes.

Padrões de projeto

A princípio poderia parecer que um projeto feito em Rails, que um framework que já deixa muita coisa pronta e exige relativamente pouco código não usaria muitos padrões de projeto. E alguns padrões tendem a não aparecer facilmente, como Abstract Factory / Factory Method, já que o dinamismo de Ruby acaba evitando muitos de seus usos.

Template Method / Factory Method

No trecho de código abaixo (tirado da classe StaffSynchronizer), temos uma aplicação do template method. Essa classe é responsável por sincronizar tanto usuários como departamentos da iniciativa com o servidor da iniciativa responsável pela administração dessas informações. O processo de sincronia pode ser escrito como:

  • Adicione todos os itens novos
  • Atualize todos os itens existentes
  • "Remova" os itens removidos - se bem que nesse caso acabou não havendo essa necessidade, ao invés disso os itens são marcados como "inativos" já que, por razões históricas, membros e departamentos que "deixaram de existir" precisam ser mantidos.

Considerando ainda que atualizar e adicionar são muito semelhantes (isto é, adicionar = criar algo novo "em branco" e atualizar), chegou-se no esqueleto do processo dividido nas funções synchronize e sync_init.

As implementações concretas do template se dão não através de classes propriamente ditas, mas fornecendo os pedaços do template "faltando" na forma de blocos de código, que a função synchronize recebe e executa.

Observe que nesse caso o bloco de código :factory é um factory method, se bem que é conhecido que o factory method é um caso particular de um template method

def synchronize existing, options = {}    
    old_map, new_map = sync_init(existing, options[:with])
 
    new_map.each do |staff_id, updated_data|
      existing_element = old_map.delete(staff_id) || options[:factory].call(updated_data)
      existing_element.staff_id = staff_id
      options[:synchronizer].call(existing_element, updated_data)
    end 
  end
 
  def sync_init old_data, new_data
    old_map = {}
    new_map = {}
 
    old_data.each do |old|
      old_map[old.staff_id] = old
    end
 
    new_data.each do |new|
      new_map[new['id'].to_i] = new
    end
 
    return old_map, new_map
  end
 
  def sync_users user_list
 
    synchronize User.find(:all),
                :with => user_list,
                :factory => lambda { User.new },
                :synchronizer => lambda { |existing_user, updated_data|
                  existing_user.name = updated_data['name']
                  existing_user.login = updated_data['username']
                  existing_user.password_hash = updated_data['password']
                  existing_user[:type] = (updated_data['administrator'] == 'true') ? Administrator.name : nil
                  existing_user.save!
 
                  update_memberships(existing_user, updated_data['memberships'])
                  existing_user.make_inactive! week_from_normal_form(updated_data['member_until']) unless updated_data['member_until'].nil?
                },
                :inactive_action => lambda { | |}
  end
 
  def sync_departments departments_list
 
    synchronize Department.find(:all),
                :with => departments_list,
                :factory => lambda { Department.new },
                :synchronizer => lambda { |existing_department, updated_data|
                  existing_department.name = updated_data['name']
                  existing_department.abbreviation = updated_data['abbreviation']
                  existing_department.save!
                }
  end

Decorator:

Apareceu uma única vez. Ziya é o plugin que gera gráficos, e aqui o decorator vem a resolver duas coisas

  • Aplicar o template de formatação apropriado automaticamente a todos os gráficos
  • Criar tooltips inteligentes com base nos valores plotados automaticamente a todos os gráficos
class ZiyaDecorator
 
  def initialize(chart, number_of_samples = 1)
    chart.add(:theme, $estilo)
    @chart = chart
    @number_of_samples = number_of_samples
  end
 
  def method_missing(m, *args, &block)
    @chart.send(m, *args, &block)
  end
 
  def add(key, *args)
    if key == :series
      if @chart.class == Ziya::Charts::Bar
        @chart.add(key, args[0], tooltipify_bar(args[1]))
      elsif @chart.class == Ziya::Charts::Column
        @chart.add(key, args[0], tooltipify_column(args[1]))
      else
        @chart.add(key, args[0], tooltipify(args[1]))
      end
    else
      @chart.add(key, *args)
    end
  end
 
  private
  def tooltipify values
    values.collect do |value|
      { :value => value, :tooltip => sprintf("%.1fh", value)}
    end
  end
 
  def tooltipify_bar values
    values.collect do |value|
      { :value => value, :tooltip => sprintf("%.1fh (Média: %.1fh)", value, value/@number_of_samples)}
    end
  end
 
  def tooltipify_column values_with_names
    values_with_names.collect do |item|
      { :value => item[0], :tooltip => sprintf("%s: %.1fh", item[1], item[0])}
    end
  end
end

Esse decorator é aplicado aos objetos Ziya::Charts::? criados quando a função create_chart (que ficou no GraphsController) é chamada para criar um gráfico

def create_chart clazz, options = {}
    if clazz == Ziya::Charts::Line
      chart = ZiyaDecorator.new(clazz.new(Configuration.ziya_license))
    else
      chart = ZiyaDecorator.new(clazz.new(Configuration.ziya_license), @range.entries.size)
    end
    chart.add(:user_data, :width, options[:width] || 500)
    chart.add(:user_data, :height, options[:height] || 250)
    chart.add(:user_data, :scroll, { :span => 1.0})
    chart
  end

Facilidades da Metaprogramação:

Vamos citar dois exemplos de como o dinamismo de Ruby reduz o volume de código do aplicativo:

1) Passar nome de método como parâmetro - dá pra fazer em Java mais exigiria um template method em C++ (:object contém o nome do método)

<h2>Membros Atuais</h2>
 
<%= render :partial => 'user_list', :object => "members" %>
 
<div style="height: 20px;">&nbsp;</div>
<h2>Ex-membros</h2>
 
<%= render :partial => 'user_list', :object => "ex_members" %>

2) Uso do method_missing

o aplicativo guarda alguns parâmetros no arquivo configuration.yml

(configuration.yml)
style: cv
staff-provider: http://127.0.0.1:3001/
ziya-license: KTAI-2HSUFV9TO25CWK-2XOI1X0-7L

Para acessar uma configuração, a classe usuária simplesmente chama Configuration.nome_da_propriedade. Trata-se de uma abordagem mais elegante do que acessar um HashMap diretamente (como poderia ser feito em Java), e que é implementada conforme abaixo. Lembrar que a idéia abaixo é de certa forma a mesma que é utilizada para implementação do padrão decorator em Ruby.

class Configuration
  class << self
    def method_missing(m, *args, &block)
      if args.size == 0 and block.nil?
        read_attribute(m.to_s.dasherize)
      else
        super
      end
    end
 
    private
      def read_attribute(attr)
        @config ||= load_config
        @config[attr]
      end
 
      def load_config
        YAML.load_file(File.dirname(__FILE__) + '/../../config/application.yml')
      end
  end
end

Esse tipo de facilidade foi usado na classe geradora de estatísticas também.

def method_missing m, *args, &b
    if m.to_s.ends_with?("_totals")
      m = m.to_s
      send(m[0...(m.size - 7)], *args).inject(0) { |s, v| s + v }
    else
      super
    end
  end

Essa classe possui uma série de métodos que retornam um array com as médias de horas trabalhas numa semana, dada alguns critérios de filtragem. Esses dados vão diretamente para o gráfico de linhas. Além desse gráfico, o programa gera também gráficos de barras com os totais acumulados.

A idéia é que se o aluno A trabalhou, nas 8 semanas de um bimestre: 1h 2h 3h 4h 5h 6h 7h 8h

Então

statistics_collector_instance = StatisticsCollector.new(range_representando_o_bimestre_acima)
statistics_collector_instance.by_user(id_do_aluno_a) #==> [1,2,3,4,5,6,7,8]
statistics_collector_instance.by_user_totals(id_do_aluno_a) #==> 36

A idéia da 2ª prova com o Range de Notas Musicais

Usamos a mesma idéia (implementar succ e <=>) na classe Week, a fim de podermos criar ranges de semanas do ITA (começando, por exemplo, na 4ª semana do 2º bimestre de 2006 e terminando numa outra semana de outro bimestre de outro ano, passando nesse meio tempo por semaninhas, semanas de exames, …)

Os trechos relevantes de código estão abaixo (para contextualizar, semaninha = semana 9 e semanas de exames = semanas 9 e 10 do 2o bi):

def to_i
    @year*100 + ((@bimester - 1) + 2*(@semester - 1))*10 + (@week - 1)
  end
 
  def succ
    if @week == 9 and @bimester == 1
      Week.new(1, 2, @semester, @year)
    elsif @week == 10
      if @semester == 1
        Week.new(1, 1, 2, @year)
      else
        Week.new(1, 1, 1, @year + 1)
      end
    else
      Week.new(@week + 1, @bimester, @semester, @year)
    end
  end
 
  def <=> other
    to_i <=> other.to_i
  end

E um exemplo de utilização abaixo:

(Se vocês voltarem no padrão Decorator, a função create_chart que está lá faz uma chamada para @range.entries.size. Aquele já é um exemplo bobo, @range é um Range de semanas e essa chamada dá o número de semanas subentendido pelo Range)

O trecho abaixo pega a lista de planilhas entregues por um usuário e coloca planilhas em branco nas semanas em que o usuário não entregou a planilha

# Chamando each para um Range
    (first_week..last_week).each do |wk|
      # Essa parte está misteriosa, principalmente  a segunda linha - entendê-la não é importante
      bimester_index = 4*(wk.year - first_week.year) + 2*(wk.semester - first_week.semester) + (wk.bimester - first_week.bimester)
      week_index = (wk.to_i/10 == first_week.to_i/10) ? (wk.week - first_week.week) : (wk.week - 1)
      # Fim da parte misteriosa
      @bimesters[bimester_index] ||= []
      if @bimesters[bimester_index][week_index].nil?
        @bimesters[bimester_index][week_index] = HourSpreadsheet.new
        @bimesters[bimester_index][week_index].week = wk
      end
    end

Problemas/Soluções no decorrer do projeto

3 pontos chamaram atenção:

A qual departamento um membro pertence?

A modelagem inicial do banco de dados atribuia cada membro a um departamento, e como isso faz sentido ninguém percebeu o problema que se criaria quando um usuário mudasse de departamento. Se o departamento dele fosse simplesmente alterado,
poderiam ser geradas anomalias nas estatísticas. A solução seria alterar essa relação 1:n (um departamento tem muitos usuários) para uma n:n (cada usuário pertence a vários departamentos em épocas diferentes), onde a tabela de JOIN conteria dados sobre
quando o membro entrou no departamento (a data de saída ficaria implicitamente determinada pela data de entrada da próxima entrada para o mesmo usuário ou seria a data de hoje se não houvesse essa próxima entrada - sair da empresa foi padronizado ser a mudança do usuáro para um departamento nulo).

Essa foi a única mudança mais profunda na estrutura do banco de dados, e que causou um impacto significativo no código do programa também. Várias funções ficaram parametrizadas (exemplo: membros no departamento X virou membros no departamento X na data tal) e a geração das estatísticas complicou. Consideremos o simples problema de contabilizar quantas horas um departamento trabalhou num período. Cada planilha pertence a um usuário, mas para qual departamento ela deveria ser contabilizada? Para o departamento que o usuário pertencia na data em que aquela planilha foi entregue. Obviamente essa linha de raciocíno se traduz em várias queries SQL, o que gerou o segundo problema

Otimizando a geração dos dados para os gráficos

Depois da refatoração acima ser feita, o gráfico que mostrava o histórico dos departamentos no bimestre levava cerca de 5 segundos para ser gerado. A razão era óbvia e podia ser constatada vendo-se os logs do Rails: a geração do gráfico estava envolvendo muitas queries SQL. A fim de reduzir esse tempo (imagine o que aconteceria em gráficos que pegassem um intervalo de 1 ano?), a primeira solução foi dar uma desnormalizada no banco de dados: cada planilha teria um campo department_id que armazenaria o departamento de seu usuário na data em que ela deveria ser entregue. Seria algo relativamente simples e seguro de ser feito, já que (em tese) o valor desse campo, uma vez armazenado em cache, nunca mais se alteraria (a não ser que alguém tenha cadastrado a data de entrada de um membro erradamente no gerenciador de usuários - o que, diga-se de passagem, não é impossível de acontecer).

Pouco tempo depois, percebemos que os quatro gráficos da primeira página podiam ser gerados de uma vez só (e daí surgiu a classe StatisticsCollector) - no fundo, para gerá-los, seria necessário, ao final de tudo, ler TODAS as planilhas do bimestre do banco de dados. O que fizemos então foi desistir de tentar achar as planilhas apropriadas para cada situação e simplesmente ler todas e categorizar as horas, o que está no código abaixo:

HourSpreadsheet.in_range(range).each do |hs|
      week_data = init_week(hs.week)
      hours_in_hs = hs.total
      user_of_hs = hs.user_id
      department_of_hs = hs.user.department_id(hs.week)
      week_data[:by_user][user_of_hs] = hours_in_hs
      week_data[:by_department][department_of_hs] ||= 0
      week_data[:by_department][department_of_hs] += hours_in_hs
      week_data[:total] += hours_in_hs
    end

Utilizando eager fetching, as estatísticas para qualquer filtro, dado um intervalo de tempo, poderiam ser geradas utilizando-se apenas três queries: uma para ler os departamentos, outra para ler todas as spreasheets (incluindo os usuários e as entradas), e outra para ler as datas de entrada de cada usuário em cada departamento. Ainda sim o volume de dados a ser processado era grande, e, apesar de serem apenas três queries, o volume de dados retornado por cada uma é razoável. Entretanto, o problema de performance foi sanado, afinal de contas no fundo essas três queries estão retornando o banco de dados inteiro (filtrado apenas por intervalo de tempo, no caso das planilhas de horas trabalhadas)

Gerenciamento de Usuários

Não seria estranho imaginar que para o banco de horas funcionar haveria a necessidade de haver um gerenciamento básico do RH da empresa (cadastro de membros e departamentos), e esse gerenciamento é um pouco mais complicado do que pareceu a princípio (vide problema 1 relativo a qual departamento um membro pertence). E, para o banco de horas funcionar, seria necessário implementar esse esqueleto.

Entretanto, duas constatações surgiram:

  • Feito esse esqueleto, falta pouco para gerar-se um cadastro completo do RH com outros dados pessoais inclusos
  • Praticamente todo software a ser desenvolvimento para essas iniciativas ia precisar desse esqueleto
  • Seria interessante que cada mudança de depto / membro entrando / membro saindo pudesse ser registrada pelo RH de cada iniciativa em um único local

A idéia então era separar as funções: o banco de horas serviria apenas como banco de horas, e haveria um aplicativo adicional para gerenciar o RH (e mais tarde esse aplicativo poderia ser cancerizado para conter mais dados de cada membro). Aí você poderia se perguntar:

1) Porque não simplesmente compartilhar a base de dados, ao invés de escrever dois programas separados?

As razões aqui seriam:

  • Porque ficaria estranho um banco de horas com a função de gerenciamento de membros e departamentos da empresa
  • Porque seria não aconselhável assumir que uma representação dos membros e departamentos no banco de dados (no caso, a que o banco de horas usou) seria adequada para todas as aplicações. Para algumas aplicações, por exemplo, bastaria guardar o departamento atual de cada membro e ex-membros poderiam ser removidos. Nesse caso, manter sincronizada uma cópia onde cada usuário pertence (simplesmente) a um departamento seria melhor.

2) Porque não gerar um aplicativozão CV Manager / ITA Jr Manager e deixar essas duas facetas como "módulos"/partes separadas?

  • Pelas mesmas razões do item (2) acima - ou ainda que fossem mantidos tabelas separadas ia virar uma zona, ou então estaria-se indiretamente fazendo cada módulo ser cliente do módulo de gerenciamento de RH
  • Porque facilita a administração de cada aplicativo - não é preciso se preocupar se há conflito de rotas, nomes de classes, tabelas no banco de dados, etc. Além disso, se um serviço cair, os outros continuam operando normalmente

Dentro da nossa saída, a parte de gerenciamento de usuários foi extraída para um novo aplicativo Rails exclusivo para esse fim. Faltaria discutir então como sincronizar o banco de horas com esse aplicativo.

Considerando que idealmente o gerenciador de RH não deva conhecer todos os seus usuários, a iniciativa de sincronismo teria que partir de cada outro aplicativo que acessa os dados do RH. Decidimos que, a cada usuário que loga, é feito um processo de sincronismo. Isso porque:

  • Garante-se que um usuário consegue logar assim que liberado pelo RH, e que ele não mais consegue logar assim que bloqueado
  • O processo de sincronismo, considerando o tamanho das bases de dados e que muito provavelmente os diferentes aplicativos estariam rodando numa mesma máquina, ou ao mesmo numa mesma rede, o tempo para a realização do processo não comprometeria o uso do software

Do lado do software de gerência do RH, as informações são expostas via chamadas API REST, no padrão XML. Do lado do banco de horas, ele resgata esses dados (se ele não encontrar o servidor do RH ele ignora o sincronismo e procede com os dados que já tem), sincronizando deleções / adições e atualizações. É isso que a classe StaffSynchronizer faz, no nosso projeto. Devido ao item (2) logo acima, o processo de sincronismo não foi cancerizado, isto é, ele é feito sempre desde o início. Em bases de dados maiores, poderia ser necessário implementar sincronismos por partes (isto é, me envie tudo o que se alterou desde dia tal), para garantir que o tempo de execução fosse razoável.

Estatísticas do Código

A princípio tentei usar o StatSVN (que é o msm que eu usei pra gerar aquelas estatísticas do trabalho do Guerra), mas aí nosso trabalho ficou com 20000 linhas de código (por causa do prototype.js). Tentei usar a opção exclude desse programa mas não deu muito certo, terminou que o bernardo lembrou que existe a tarefa rake stats que gera algumas estatísticas para projetos feito em Rails. Seguem abaixo os resultados

Banco de Horas

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |   455 |   309 |       6 |      33 |   5 |     7 |
| Helpers              |    34 |    30 |       0 |       3 |   0 |     8 |
| Models               |   427 |   352 |      10 |      41 |   4 |     6 |
| Libraries            |   292 |   239 |       3 |      31 |  10 |     5 |
| Integration tests    |     0 |     0 |       0 |       0 |   0 |     0 |
| Functional tests     |    33 |    24 |       3 |       4 |   1 |     4 |
| Unit tests           |   315 |   247 |       2 |      12 |   6 |    18 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |  1556 |  1201 |      24 |     124 |   5 |     7 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 930     Test LOC: 271     Code to Test Ratio: 1:0.3

Cadastro do RH

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |   155 |   115 |       5 |      17 |   3 |     4 |
| Helpers              |    32 |    28 |       0 |       3 |   0 |     7 |
| Models               |   233 |   193 |       6 |      22 |   3 |     6 |
| Libraries            |     0 |     0 |       0 |       0 |   0 |     0 |
| Integration tests    |     0 |     0 |       0 |       0 |   0 |     0 |
| Functional tests     |    32 |    24 |       4 |       4 |   1 |     4 |
| Unit tests           |    24 |    18 |       3 |       3 |   1 |     4 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |   476 |   378 |      18 |      49 |   2 |     5 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 336     Test LOC: 42     Code to Test Ratio: 1:0.1

Como Instalar

  • Instalar o Ruby 1.8.6
  • Instalar o Rails (gem install rails —version=2.1.2 —include-dependencies). Caso já tenha uma versão mais nova instalada, o app deve funcionar, mas aí será necessário editar o environment.rb
  • Instalar o Ziya (gem install ziya)
  • Fazer o checkout do projeto a partir de http://hours-bank.googlecode.com/svn/trunk/
    • O Banco de Horas estará no diretório HoursBank/
    • O Cadastro do RH estará na pasta Staff/
  • Configurar os parâmetros de acesso aos bancos de dados nos database.yml (arquivo config/database.yml de cada um dos projetos)
  • Configurar o banco de horas (arquivo config/application.yml)
    • staff-provider é o endereço web a partir do qual o cadastro do RH será acessado (se deixar incorretamente configurado nunca haverá sincronização de um com o outro)
    • style é o estilo usado para formatação (se vc baixou do svn recomenda-se usar 'cv')
    • ziya-license é o código para desbloquear a biblioteca de gráficos que o Ziya usa (XML/SWF Charts), pode deixar com o valor que vier do svn se vc for rodar o programa no seu computador (localhost)
  • Rodar o cadastro do RH (abrir um prompt de comando na pasta do projeto e digitar ruby script/server —port=[porta que vc configurou no application.yml])
  • Cadastrar departamentos e usuários
  • Ainda com o cadastro do RH rodando, fazer logon no HoursBank (para fazê-lo rodar, ruby script/server também) com qualquer um dos usuários que você cadastrou (isso sincronizará automaticamente as bases de dados, caso você tenha configurado tudo certo).

Importante: o cadastro do RH e o banco de horas NÃO PODEM compartilhar o mesmo banco de dados (eles podem sim ser colocados no mesmo servidor SQL, mas não no mesmo banco de dados)

Conclusão

Sem maiores enrolações, resumimos aqui os principais resultados desse trabalho:

  • Mesmo para usuários experientes, o aprendizado de padrões de projeto se traduz em melhor qualidade de código
  • É um desafio fazer um aplicativo de uso real a título de exame - nosso projeto conta com algumas milhares de linhas de código já e não está exatamente pronto. Uma coisa que se percebe é que se isso fosse simplesmente o projeto exame e não fosse ser usado depois, poderíamos ter "agasalhado" algumas limitações, como cada membro poder estar em um único depto, mas como queremos usar isso aqui a história é outra. Esse aplicativo não parece complexo, mesmo assim acreditamos que poderíamos ter escolhido algo ainda mais simples (em termos de escopo) para objeto do exame
  • ActiveRecord é muuuuito mais fácil de usar do que Hibernate
  • O Internet Explorer é suga - se tudo pudesse ter que funcionar só no Firefox teríamos muito menos trabalho - e olha que o projeto, do jeito que está agora, talvez não rode no IE. Em particular, perdemos uma madrugada inteira para contornar o fato de que o método cloneNode (da implementação DOM em JavaScript) não funciona no IE quando são copiados combo boxes, check boxes e mais não lembro o que (e era justamente nosso caso). Isso sem contar a falta de conformidade no processamento do CSS
Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License