[webui] another fix for repository state - this time for repos
[opensuse:build-service.git] / src / webui / app / controllers / project_controller.rb
1 require 'project_status'
2 require 'collection'
3 require 'buildresult'
4 require 'role'
5 require 'models/package'
6
7 include ActionView::Helpers::UrlHelper
8 include ApplicationHelper
9 include RequestHelper
10
11 class ProjectController < ApplicationController
12
13   class NoChangesError < Exception; end
14
15   before_filter :require_project, :only => [:delete, :buildresult, :view,
16     :edit, :save, :add_target_simple, :save_target, :status, :prjconf,
17     :remove_person, :save_person, :add_person, :remove_target, :toggle_watch,
18     :show, :monitor, :edit_prjconf, :list_requests, :autocomplete_packages,
19     :packages, :users, :subprojects, :repositories, :attributes, :edit_repository,
20     :new_package, :new_package_link, :patchinfo, :repository_state,
21     :meta, :edit_meta, :save_meta, :edit_comment, :change_flag, :save_targets, :autocomplete_repositories ]
22
23   before_filter :load_current_requests, :only => [:delete, :view,
24     :edit, :save, :add_target_simple, :save_target, :status, :prjconf,
25     :remove_person, :save_person, :add_person, :remove_target,
26     :show, :monitor, :edit_prjconf, :list_requests,
27     :packages, :users, :subprojects, :repositories, :attributes, :meta, :edit_meta ]
28   before_filter :require_prjconf, :only => [:edit_prjconf, :prjconf ]
29   before_filter :require_meta, :only => [:edit_meta, :meta ]
30
31   def index
32     redirect_to :action => 'list_public'
33   end
34
35   def list_all
36     list and return
37   end
38
39   def list_public
40     params['excludefilter'] = 'home:'
41     list and return
42   end
43
44   def list
45     @important_projects = get_important_projects
46     @filterstring = params[:searchtext] || ''
47     @excludefilter = params['excludefilter'] if params['excludefilter'] and params['excludefilter'] != 'undefined'
48     get_filtered_projectlist @filterstring, @excludefilter
49     if request.xhr?
50       render :partial => 'search_project', :locals => {:project_list => @projects} and return
51     end
52     render :list, :status => params[:nextstatus]
53   end
54
55   def autocomplete_projects
56     get_filtered_projectlist params[:q], ''
57     render :text => @projects.join("\n")
58   end
59
60   def autocomplete_repositories
61     @repos = @project.repositories
62     render :text => @repos.join("\n")
63   end
64
65   def project_key(a)
66     a = a.downcase
67
68     if a[0..4] == 'home:'
69       a = 'zz' + a
70     end
71     return a
72   end
73   private :project_key
74
75   def get_filtered_projectlist(filterstring, excludefilter='')
76     # remove illegal xpath characters
77     filterstring.sub!(/[\[\]\n]/, '')
78     filterstring.sub!(/[']/, '&apos;')
79     filterstring.sub!(/["]/, '&quot;')
80     predicate = filterstring.empty? ? '' : "contains(@name, '#{filterstring}')"
81     predicate += " and " if !predicate.empty? and !excludefilter.blank?
82     predicate += "not(starts-with(@name,'#{excludefilter}'))" if !excludefilter.blank?
83     result = find_cached Collection, :id, :what => "project", :predicate => predicate, :expires_in => 2.minutes
84     @projects = Array.new
85     result.each { |p| @projects << p.name }
86     @projects =  @projects.sort_by { |a| project_key a }
87   end
88   private :get_filtered_projectlist
89
90   def users
91     @email_hash = Hash.new
92     @project.each_person do |person|
93       @email_hash[person.userid.to_s] = find_cached(Person, person.userid, :expires_in => 30.minutes ).email.to_s
94     end
95     @roles = Role.local_roles
96   end
97
98   def subprojects
99     @subprojects = Hash.new
100     sub_names = Collection.find :id, :what => "project", :predicate => "starts-with(@name,'#{@project}:')"
101     sub_names.each do |sub|
102       @subprojects[sub.name] = find_cached( Project, sub.name )
103     end
104     @parentprojects = Hash.new
105     parent_names = @project.name.split ':'
106     parent_names.each_with_index do |parent, idx|
107       parent_name = parent_names.slice(0, idx+1).join(':')
108       unless [@project.name, 'home'].include?( parent_name )
109         parent_project = find_cached(Project, parent_name )
110         @parentprojects[parent_name] = parent_project unless parent_project.blank?
111       end
112     end
113   end
114
115   def attributes
116     @attributes = Attribute.find(:project, :project => params[:project])
117   end
118
119   def new
120     @namespace = params[:ns]
121     @project_name = params[:project]
122     if params[:ns] == "home:#{session[:login]}"
123       @project = find_cached Project, params[:ns]
124       unless @project
125         flash.now[:note] = "Your home project doesn't exist yet. You can create it now by entering some" +
126           " descriptive data and press the 'Create Project' button."
127         @project_name = params[:ns]
128       end
129     end
130     if @project_name =~ /home:(.+)/
131       @project_title = "#$1's Home Project"
132     else
133       @project_title = ""
134     end
135   end
136
137   def show
138     @bugowner_mail = find_cached(Person, @project.bugowner ).email.to_s if @project.bugowner
139     @packages = Rails.cache.fetch("%s_packages_mainpage" % @project, :expires_in => 30.minutes) do
140       find_cached(Package, :all, :project => @project.name, :expires_in => 30.seconds )
141     end
142
143     @problem_packages = Rails.cache.fetch("%s_problem_packages" % @project, :expires_in => 30.minutes) do
144       buildresult = find_cached(Buildresult, :project => @project, :view => 'status', :code => ['failed', 'broken', 'unresolvable'], :expires_in => 2.minutes )
145       if buildresult
146         results = buildresult.data.find( 'result/status' )
147         results.map{|e| e.attributes['package'] }.uniq.size
148       else
149         0
150       end
151     end
152
153     load_buildresult
154
155     render :show, :status => params[:nextstatus] if params[:nextstatus]
156   end
157
158   # TODO we need the architectures in api/distributions
159   def add_target_simple
160     dist_xml = Rails.cache.fetch("distributions", :expires_in => 30.minutes) do
161       frontend = ActiveXML::Config::transport_for( :package )
162       frontend.direct_http URI("/distributions"), :method => "GET"
163     end
164     @distributions = XML::Document.string dist_xml
165   end
166
167   def add_person
168     @roles = Role.local_roles
169   end
170
171   def load_buildresult(cache = true)
172     unless cache
173       Buildresult.free_cache( :project => params[:project], :view => 'summary' )
174     end
175     @buildresult = find_cached(Buildresult, :project => params[:project], :view => 'summary', :expires_in => 3.minutes )
176
177     @repohash = Hash.new
178     @statushash = Hash.new
179     @repostatushash = Hash.new
180     @packagenames = Array.new
181
182     @buildresult.each_result do |result|
183       repo = result.repository
184       arch = result.arch
185
186       # repository status cache
187       @repostatushash[repo] ||= Hash.new
188       @repostatushash[repo][arch] = Hash.new
189
190       if result.has_attribute? :state
191         if result.has_attribute? :dirty
192           @repostatushash[repo][arch] = "outdated_" + result.state.to_s
193         else
194           @repostatushash[repo][arch] = result.state.to_s
195         end
196       end
197     end if @buildresult
198     if @buildresult
199       @buildresult = @buildresult.to_a
200     else
201       @buildresult = Array.new
202     end
203   end
204
205   def buildresult
206     unless request.xhr?
207       render :text => 'no ajax', :status => 400 and return
208     end
209     load_buildresult false
210     render :partial => 'buildstatus'
211   end
212
213   def delete
214     valid_http_methods :post
215     @confirmed = params[:confirmed]
216     if @confirmed == "1"
217       begin
218         if params[:force] == "1"
219           @project.delete :force => 1
220         else
221           @project.delete
222         end
223         Rails.cache.delete("%s_packages_mainpage" % @project)
224         Rails.cache.delete("%s_problem_packages" % @project)
225       rescue ActiveXML::Transport::Error => err
226         @error, @code, @api_exception = ActiveXML::Transport.extract_error_message err
227         logger.error "Could not delete project #{@project}: #{@error}"
228       end
229     end
230   end
231
232   def arch_list
233     if @arch_list.nil?
234       tmp = []
235       @project.each_repository do |repo|
236         tmp += repo.archs
237       end
238       @arch_list = tmp.sort.uniq
239     end
240     return @arch_list
241   end
242
243   def edit_repository
244     repo = @project.repository[params[:repository]]
245     @arch_list = arch_list
246     render :partial => 'edit_repository', :locals => { :repository => repo, :error => nil }
247   end
248
249   def update_target
250     valid_http_methods :post
251     repo = @project.repository[params[:repo]]
252     repo.name = params[:name]
253     repo.archs = params[:arch].to_a
254     @arch_list = arch_list
255     begin
256       @project.save
257       render :partial => 'repository_item', :locals => { :repo => repo, :has_data => true }
258     rescue => e
259       repo.name = params[:original_name]
260       render :partial => 'repository_edit_form', :locals => { :error => "#{ActiveXML::Transport.extract_error_message( e )[0]}",
261         :repo => repo } and return
262     end
263   end
264
265   def repositories
266     # overwrite @project with different view
267     # TODO to get this cached we need to make sure it gets purged on repo updates
268     @project = Project.find( params[:project], :view => :flagdetails )
269   end
270
271   def repository_state
272     # Get cycles of the repository build dependency information
273     # 
274     @repocycles = Hash.new
275     @repositories = Array.new
276     if params[:repository]
277       @repositories << params[:repository]
278     elsif @project.has_element? :repository
279       @project.each_repository { |repository| @repositories << repository.name }
280     end
281    
282     @project.each_repository do |repository| 
283       next unless @repositories.include? repository.name
284       @repocycles[repository.name] = Hash.new
285          
286       repository.each_arch do |arch|
287
288         cycles = Array.new
289         begin
290           p=params
291           # skip all packages via package=- to speed up the api call, we only parse the cycles anyway
292           p[:package]="-"
293           p[:repository]=repository.name
294           p[:arch]=arch
295           deps = ActiveXML::XMLNode.new( frontend.get_builddepinfo(p) )
296           if deps.has_element? :cycle
297             cycles = Array.new
298             deps.each_cycle do |cycle|
299               cycles.push( cycle.each_package.collect{ |p| p.text } )
300             end
301           end
302         rescue ActiveXML::Transport::NotFoundError
303           # builddepinfo not yet calculated by scheduler
304           cycles.push( [ 'unknown' ] )
305         end
306         if cycles.length > 0
307           @repocycles[repository.name][arch.text] = cycles
308         end
309       end
310     end
311   end
312
313   def load_packages
314     @packages = find_cached(Package, :all, :project => @project.name, :expires_in => 30.seconds )
315   end
316
317   def packages
318     load_packages
319     # push to long time cache for the project frontpage
320     Rails.cache.write("#{@project}_packages_mainpage", @packages, :expires_in => 30.minutes)
321     @patchinfo = []
322     @packages.each do |p|
323       @patchinfo << p.name if p.name =~ %r{^_patchinfo}
324     end
325   end
326
327   def autocomplete_packages
328     packages
329     render :text => @packages.each.select{|p| p.name.index(params[:q]) }.map{|p| p.name}.join("\n")
330   end
331
332   def list_requests
333   end
334
335   def save_new
336     if params[:name].blank? || !valid_project_name?( params[:name] )
337       flash[:error] = "Invalid project name '#{params[:name]}'."
338       redirect_to :action => "new", :ns => params[:ns] and return
339     end
340
341     project_name = params[:name].strip
342     project_name = params[:ns].strip + ":" + project_name.strip if params[:ns]
343
344     if Project.exists? project_name
345       flash[:error] = "Project '#{project_name}' already exists."
346       redirect_to :action => "new", :ns => params[:ns] and return
347     end
348
349     #store project
350     @project = Project.new(:name => project_name)
351     @project.title.text = params[:title]
352     @project.description.text = params[:description]
353     @project.set_remoteurl(params[:remoteurl]) if params[:remoteurl]
354     @project.add_person :userid => session[:login], :role => 'maintainer'
355     @project.add_person :userid => session[:login], :role => 'bugowner'
356     begin
357       if @project.save
358         flash[:note] = "Project '#{@project}' was created successfully"
359         redirect_to :action => 'show', :project => project_name and return
360       else
361         flash[:error] = "Failed to save project '#{@project}'"
362       end
363     rescue ActiveXML::Transport::ForbiddenError => err
364       flash[:error] = "You lack the permission to create the project '#{@project}'. " +
365         "Please create it in your home:%s namespace" % session[:login]
366       redirect_to :action => 'new', :ns => "home:" + session[:login] and return
367     end
368     redirect_to :action => 'new'
369   end
370
371   def save
372     if ( !params[:title] )
373       flash[:error] = "Title must not be empty"
374       redirect_to :action => 'edit', :project => params[:project]
375       return
376     end
377
378     @project.title.text = params[:title]
379     @project.description.text = params[:description]
380
381     if @project.save
382       flash[:note] = "Project '#{@project}' was saved successfully"
383     else
384       flash[:error] = "Failed to save project '#{@project}'"
385     end
386
387     redirect_to :action => :show, :project => @project
388   end
389
390
391   def save_targets
392     valid_http_methods :post
393     if (params['repo'].blank?)
394       flash[:error] = "Please select a repository."
395       redirect_to :action => :add_target_simple, :project => @project and return
396     end
397
398     params['repo'].each do |repo|
399       if !valid_target_name? repo
400         flash[:error] = "Illegal target name #{repo}."
401         redirect_to :action => :add_target_simple, :project => @project and return
402       end
403       repo_path = params[repo + '_repo'] || "#{params['target_project']}/#{params['target_repo']}"
404       repo_archs = params[repo + '_arch'] || params['arch']
405       logger.debug "Adding repo: #{repo_path}, archs: #{repo_archs}"
406       @project.add_repository :reponame => repo, :repo_path => repo_path, :arch => repo_archs
407
408       # FIXME: will be cleaned up after implementing FATE #308899
409       if repo == "images"
410         prjconf = frontend.get_source(:project => @project, :filename => '_config')
411         unless prjconf =~ /^Type:/
412           prjconf = "%if \"%_repository\" == \"images\"\nType: kiwi\nRepotype: none\nPatterntype: none\n%endif\n" << prjconf
413           frontend.put_file(prjconf, :project => @project, :filename => '_config')
414         end
415       end
416     end
417
418     begin
419       if @project.save
420         flash[:note] = "Build targets were added successfully"
421       else
422         flash[:error] = "Failed to add build targets"
423       end
424     rescue ActiveXML::Transport::Error => e
425       message, code, api_exception = ActiveXML::Transport.extract_error_message e
426       flash[:error] = "Failed to add build targets: " + message
427     end
428     redirect_to :action => :repositories, :project => @project
429   end
430
431
432   def remove_target
433     valid_http_methods :post
434     if not params[:target]
435       flash[:error] = "Target removal failed, no target selected!"
436       redirect_to :action => :show, :project => params[:project]
437     end
438     @project.remove_repository params[:target]
439
440     begin
441       if @project.save
442         flash[:note] = "Target '#{params[:target]}' was removed"
443       else
444         flash[:error] = "Failed to remove target '#{params[:target]}'"
445       end
446     rescue ActiveXML::Transport::Error => e
447       message, code, api_exception = ActiveXML::Transport.extract_error_message e
448       flash[:error] = "Failed to remove target '#{params[:target]}' " + message
449     end
450
451     redirect_to :action => :repositories, :project => @project
452   end
453
454
455   def save_person
456     valid_http_methods(:post)
457     user = find_cached( Person, params[:userid] )
458     unless user
459       flash[:error] = "Unknown user with id '#{params[:userid]}'"
460       redirect_to :action => :add_person, :project => @project, :role => params[:role]
461       return
462     end
463     @project.add_person( :userid => user.login.to_s, :role => params[:role] )
464     if @project.save
465       flash[:note] = "Added user #{user.login} with role #{params[:role]} to project #{@project}"
466     else
467       flash[:error] = "Failed to add user '#{params[:userid]}'"
468     end
469     redirect_to :action => :users, :project => @project
470   end
471
472
473   def remove_person
474     if params[:userid].blank?
475       flash[:note] = "User removal aborted, no user id given!"
476       redirect_to :action => :show, :project => params[:project] and return
477     end
478     @project.remove_persons( :userid => params[:userid], :role => params[:role] )
479     if @project.save
480       flash[:note] = "Removed user #{params[:userid]}"
481     else
482       flash[:error] = "Failed to remove user '#{params[:userid]}'"
483     end
484     redirect_to :action => :users, :project => params[:project]
485   end
486
487
488   def monitor
489     @name_filter = params[:pkgname]
490     @lastbuild_switch = params[:lastbuild]
491     if params[:defaults]
492       defaults = (Integer(params[:defaults]) rescue 1) > 0
493     else
494       defaults = true
495     end
496     params['expansionerror'] = 1 if params['unresolvable']
497     @avail_status_values = Buildresult.avail_status_values
498     @filter_out = ['disabled', 'excluded', 'unknown']
499     @status_filter = []
500     @avail_status_values.each { |s|
501       id=s.gsub(' ', '')
502       if params.has_key?(id)
503         next unless (Integer(params[id]) rescue 1) > 0
504       else
505         next unless defaults
506       end
507       next if defaults && @filter_out.include?(s)
508       @status_filter << s
509     }
510
511     @avail_arch_values = []
512     @avail_repo_values = []
513
514     @project.each_repository { |r|
515       @avail_repo_values << r.name
516       @avail_arch_values << r.archs if r.archs
517     }
518     @avail_arch_values = @avail_arch_values.flatten.uniq.sort
519     @avail_repo_values = @avail_repo_values.flatten.uniq.sort
520
521     @arch_filter = []
522     @avail_arch_values.each { |s|
523       archid = valid_xml_id('arch_' + s)
524       if defaults || (params.has_key?(archid) && params[archid])
525         @arch_filter << s
526       end
527     }
528
529     @repo_filter = []
530     @avail_repo_values.each { |s|
531       repoid = valid_xml_id('repo_' + s)
532       if defaults || (params.has_key?(repoid) && params[repoid])
533         @repo_filter << s
534       end
535     }
536
537     find_opt = { :project => @project, :view => 'status', :code => @status_filter,
538       :arch => @arch_filter, :repo => @repo_filter }
539     find_opt[:lastbuild] = 1 unless @lastbuild_switch.blank?
540
541     @buildresult = Buildresult.find( find_opt )
542     unless @buildresult
543       flash[:error] = "No build results for project '#{@project}'"
544       redirect_to :action => :show, :project => params[:project]
545       return
546     end
547
548     if not @buildresult.has_element? :result
549       @buildresult_unavailable = true
550       return
551     end
552
553     @repohash = Hash.new
554     @statushash = Hash.new
555     @repostatushash = Hash.new
556     @packagenames = Array.new
557
558     @buildresult.each_result do |result|
559       @resultvalue = result
560       repo = result.repository
561       arch = result.arch
562
563       next unless @repo_filter.include? repo
564       @repohash[repo] ||= Array.new
565       next unless @arch_filter.include? arch
566       @repohash[repo] << arch
567
568       # package status cache
569       @statushash[repo] ||= Hash.new
570
571       stathash = Hash.new
572       result.each_status do |status|
573         stathash[status.package.to_s] = status
574       end
575       stathash.keys.each do |p|
576         @packagenames << p.to_s
577       end
578
579       @statushash[repo][arch] = stathash
580
581       # repository status cache
582       @repostatushash[repo] ||= Hash.new
583       @repostatushash[repo][arch] = Hash.new
584
585       if result.has_attribute? :state
586         if result.has_attribute? :dirty
587           @repostatushash[repo][arch] = "outdated_" + result.state.to_s
588         else
589           @repostatushash[repo][arch] = result.state.to_s
590         end
591       end
592     end
593     @packagenames = @packagenames.flatten.uniq.sort
594
595     ## Filter for PackageNames ####
596     @packagenames.reject! {|name| not filter_matches?(name,@name_filter) } if not @name_filter.blank?
597     packagename_hash = Hash.new
598     @packagenames.each { |p| packagename_hash[p.to_s] = 1 }
599
600     # filter out repos without current packages
601     @statushash.each do |repo, hash|
602       hash.each do |arch, packages|
603
604         has_packages = false
605         packages.each do |p, status|
606           if packagename_hash.has_key? p
607             has_packages = true
608             break
609           end
610         end
611         unless has_packages
612           @repohash[repo].delete arch
613         end
614       end
615     end
616   end
617
618   def filter_matches?(input,filter_string)
619     result = false
620     filter_string.gsub!(/\s*/,'')
621     filter_string.split(',').each { |filter|
622       no_invert = filter.match(/(^!?)(.+)/)
623       if no_invert[1] == '!'
624         result = input.include?(no_invert[2]) ? result : true
625       else
626         result = input.include?(no_invert[2]) ? true : result
627       end
628     }
629     return result
630   end
631
632   # should be in the package controller, but all the helper functions to render the result of a build are in the project
633   def package_buildresult
634     unless request.xhr?
635       render :text => 'no ajax', :status => 400 and return
636     end
637
638     @project = params[:project]
639     @package = params[:package]
640     begin
641       @buildresult = find_cached(Buildresult, :project => params[:project], :package => params[:package], :view => 'status', :lastbuild => 1, :expires_in => 2.minutes )
642     rescue ActiveXML::Transport::Error # wild work around for backend bug (sends 400 for 'not found')
643     end
644     @repohash = Hash.new
645     @statushash = Hash.new
646
647     @buildresult.each_result do |result|
648       repo = result.repository
649       arch = result.arch
650
651       @repohash[repo] ||= Array.new
652       @repohash[repo] << arch
653
654       # package status cache
655       @statushash[repo] ||= Hash.new
656       @statushash[repo][arch] = Hash.new
657
658       stathash = @statushash[repo][arch]
659       result.each_status do |status|
660         stathash[status.package] = status
661       end
662     end if @buildresult
663     render :layout => false
664   end
665
666   def toggle_watch
667     render :update do |page|
668       if @user.watches? @project.name
669         logger.debug "Remove #{@project} from watchlist for #{@user}"
670         @user.remove_watched_project @project.name
671         page << "$('#imgwatch').attr('src', '../images/magnifier_zoom_in.png');"
672         page << "$('#imgwatch').attr('title', 'Watch this project');"
673         page << "$('#watchlist_#{Digest::MD5.hexdigest @project.name}').remove(); "
674       else
675         logger.debug "Add #{@project} to watchlist for #{@user}"
676         @user.add_watched_project @project.name
677         page << "$('#imgwatch').attr('src', '../images/magnifier_zoom_out.png');"
678         page << "$('#imgwatch').attr('title', \"Don't watch this project\");"
679         page << "$('#menu-favorites').append('<li id=\"watchlist_#{ Digest::MD5.hexdigest @project.name }\">#{link_to @project, {:controller => 'project', :action => :show, :project => @project}}</li>'); "
680       end
681     end
682     @user.save
683     Person.free_cache( :login => session[:login] )
684   end
685
686   def edit_meta
687     render :template => "project/edit_meta"
688   end
689
690   def meta
691   end
692
693   def prjconf
694   end
695
696   def edit_prjconf
697   end
698
699   def change_flag
700     if request.post? and params[:cmd] and params[:flag]
701       frontend.source_cmd params[:cmd], :project => @project, :repository => params[:repository], :arch => params[:arch], :flag => params[:flag], :status => params[:status]
702     end
703     Project.free_cache( :name => params[:project], :view => :flagdetails )
704     if request.xhr?
705       @project = find_cached(Project, :name => params[:project], :view => :flagdetails )
706       render :partial => 'shared/repositories_flag_table', :locals => { :flags => @project.send(params[:flag]), :obj => @project }
707     else
708       redirect_to :action => :repositories, :project => @project
709     end
710   end
711
712   def save_prjconf
713     frontend.put_file(params[:config], :project => params[:project], :filename => '_config')
714     flash[:note] = "Project Config successfully saved"
715     redirect_to :action => :prjconf, :project => params[:project]
716   end
717
718   def save_meta
719     begin
720       frontend.put_file(params[:meta], :project => params[:project], :filename => '_meta')
721     rescue ActiveXML::Transport::Error => e
722       message, code, api_exception = ActiveXML::Transport.extract_error_message e
723       flash[:error] = message
724       @meta = params[:meta]
725       edit_meta
726       return
727     end
728
729     flash[:note] = "Config successfully saved"
730     Project.free_cache params[:project]
731     redirect_to :action => :meta, :project => params[:project]
732   end
733
734   def clear_failed_comment
735     # TODO(Jan): put this logic in the Attribute model
736     transport ||= ActiveXML::Config::transport_for(:package)
737     params["package"].to_a.each do |p|
738       begin
739         transport.direct_http URI("/source/#{params[:project]}/#{p}/_attribute/OBS:ProjectStatusPackageFailComment"), :method => "DELETE"
740       rescue ActiveXML::Transport::ForbiddenError => e
741         message, code, api_exception = ActiveXML::Transport.extract_error_message e
742         flash[:error] = message
743         redirect_to :action => :status, :project => params[:project]
744         return
745       end
746     end
747     if request.xhr?
748       render :text => '<em>Cleared comment</em>'
749       return
750     end
751     if params["package"].to_a.length > 1
752       flash[:note] = "Cleared comment for packages %s" % params[:package].to_a.join(',')
753     else
754       flash[:note] = "Cleared comment for package #{params[:package]}"
755     end
756     redirect_to :action => :status, :project => params[:project]
757   end
758
759   def edit_comment_form
760     @comment = params[:comment]
761     @project = params[:project]
762     @package = params[:package]
763     render :partial => "edit_comment_form"
764   end
765
766   def edit_comment
767     @package = params[:package]
768     attr = Attribute.new(:project => params[:project], :package => params[:package])
769     attr.set('OBS', 'ProjectStatusPackageFailComment', params[:text])
770     result = attr.save
771     @result = result
772     if result[:type] == :error
773       @comment = params[:last_comment]
774     else
775       @comment = params[:text]
776     end
777     render :partial => "edit_comment"
778   end
779
780   def get_changes_md5(project, package)
781     begin
782       dir = find_cached(Directory, :project => project, :package => package, :expand => "1")
783     rescue => e
784       dir = nil
785     end
786     return nil unless dir
787     changes = []
788     dir.each_entry do |e|
789       name = e.name.to_s
790       if name =~ /.changes$/
791         if name == package + ".changes"
792           return e.md5.to_s
793         end
794         changes << e.md5.to_s
795       end
796     end
797     if changes.size == 1
798       return changes[0]
799     end
800     logger.debug "can't find unique changes file: " + dir.dump_xml
801     raise NoChangesError, "no .changes file in #{project}/#{package}"
802   end
803   private :get_changes_md5
804
805   def changes_file_difference(project1, package1, project2, package2)
806     md5_1 = get_changes_md5(project1, package1)
807     md5_2 = get_changes_md5(project2, package2)
808     return md5_1 != md5_2
809   end
810   private :changes_file_difference
811
812   def status
813     status = Rails.cache.fetch("status_%s" % @project, :expires_in => 10.minutes) do
814       ProjectStatus.find(:project => @project)
815     end
816     unless status
817       # a project without package and repos will not do
818       # should be handled more graceful in API, but this is WIP and google
819       # crawls a lot of these links (TODO)
820       render_error :message => "No status for this project", :status => 400
821       return
822     end
823
824     all_packages = "All Packages"
825     no_project = "No Project"
826     @current_develproject = params[:filter_devel] || all_packages
827     @ignore_pending = params[:ignore_pending] || false
828     @limit_to_fails = !(!params[:limit_to_fails].nil? && params[:limit_to_fails] == 'false')
829     @include_versions = !(!params[:include_versions].nil? && params[:include_versions] == 'false')
830
831     attributes = find_cached(PackageAttribute, :namespace => 'OBS',
832       :name => 'ProjectStatusPackageFailComment', :project => @project, :expires_in => 2.minutes)
833     comments = Hash.new
834     attributes.data.find('/attribute/project/package/values').each do |p|
835       # unfortunately libxml's find_first does not work on nodes, but on document (known bug)
836       p.each_element do |v|
837         comments[p.parent['name']] = v.content
838       end
839     end
840
841     upstream_versions = Hash.new
842     upstream_urls = Hash.new
843
844     if @include_versions
845       attributes = find_cached(PackageAttribute, :namespace => 'openSUSE',
846         :name => 'UpstreamVersion', :project => @project, :expires_in => 2.minutes)
847       attributes.data.find('//package//values').each do |p|
848         # unfortunately libxml's find_first does not work on nodes, but on document (known bug)
849         p.each_element do |v|
850           upstream_versions[p.parent['name']] = v.content
851         end
852       end
853
854       attributes = find_cached(PackageAttribute, :namespace => 'openSUSE',
855         :name => 'UpstreamTarballURL', :project => @project, :expires_in => 2.minutes)
856       attributes.data.find('//package//values').each do |p|
857         # unfortunately libxml's find_first does not work on nodes, but on document (known bug)
858         p.each_element do |v|
859           upstream_urls[p.parent['name']] = v.content
860         end
861       end
862     end
863
864     raw_requests = Rails.cache.fetch("requests_new", :expires_in => 5.minutes) do
865       Collection.find(:what => 'request', :predicate => "(state/@name='new')")
866     end
867
868     @requests = Hash.new
869     submits = Hash.new
870     raw_requests.each_request do |r|
871       id = Integer(r.data['id'])
872       @requests[id] = r
873       #logger.debug r.dump_xml + " " + (r.has_element?('action') ? r.action.data['type'] : "false")
874       if r.has_element?('action') && r.action.data['type'] == "submit"
875         target = r.action.target.data
876         key = target['project'] + "/" + target['package']
877         submits[key] ||= Array.new
878         submits[key] << id
879       end
880     end
881
882     @develprojects = Array.new
883
884     @packages = Array.new
885     status.each_package do |p|
886       currentpack = Hash.new
887       currentpack['name'] = p.name
888       currentpack['failedcomment'] = comments[p.name] if comments.has_key? p.name
889       newest = 0
890
891       p.each_failure do |f|
892         next if f.repo =~ /ppc/
893         next if f.repo =~ /staging/
894         next if f.repo =~ /snapshot/
895         next if newest > (Integer(f.time) rescue 0)
896         next if f.srcmd5 != p.srcmd5
897         currentpack['failedarch'] = f.repo.split('/')[1]
898         currentpack['failedrepo'] = f.repo.split('/')[0]
899         newest = Integer(f.time)
900         currentpack['firstfail'] = newest
901       end
902
903       currentpack['problems'] = Array.new
904       currentpack['requests_from'] = Array.new
905       currentpack['requests_to'] = Array.new
906
907       key = @project.name + "/" + p.name
908       if submits.has_key? key
909         currentpack['requests_from'].concat(submits[key])
910       end
911
912       currentpack['version'] = p.version
913       if upstream_versions.has_key? p.name
914         upstream_version = upstream_versions[p.name]
915         if p.version < upstream_version
916           currentpack['upstream_version'] = upstream_version
917           currentpack['upstream_url'] = upstream_urls[p.name] if upstream_urls.has_key? p.name
918         end
919       end
920
921       currentpack['md5'] = p.srcmd5
922
923       if p.has_element? :develpack
924         @develprojects << p.develpack.proj
925         currentpack['develproject'] = p.develpack.proj
926         if (@current_develproject != p.develpack.proj or @current_develproject == no_project) and @current_develproject != all_packages
927           next
928         end
929         currentpack['develpackage'] = p.develpack.pack
930         key = "%s/%s" % [p.develpack.proj, p.develpack.pack]
931         if submits.has_key? key
932           currentpack['requests_to'].concat(submits[key])
933         end
934         currentpack['develmd5'] = p.develpack.package.srcmd5
935
936         if currentpack['md5'] and currentpack['develmd5'] and currentpack['md5'] != currentpack['develmd5']
937           currentpack['problems'] << Rails.cache.fetch("dd_%s_%s" % [currentpack['md5'], currentpack['develmd5']]) do
938             begin
939               if changes_file_difference(@project.name, p.name, currentpack['develproject'], currentpack['develpackage'])
940                 'different_changes'
941               else
942                 'different_sources'
943               end
944             rescue NoChangesError => e
945               e.message
946             end
947           end
948         end
949         if p.develpack.package.has_element? :error
950           currentpack['problems'] << 'error-' + p.develpack.package.error.to_s
951         end
952       elsif @current_develproject != no_project
953         next if @current_develproject != all_packages
954       end
955
956       next if !currentpack['requests_from'].empty? and @ignore_pending
957       if @limit_to_fails
958         next if !currentpack['firstfail']
959       else
960         next unless (currentpack['firstfail'] or currentpack['failedcomment'] or currentpack['upstream_version'] or
961             !currentpack['problems'].empty? or !currentpack['requests_from'].empty? or !currentpack['requests_to'].empty?)
962       end
963       @packages << currentpack
964     end
965
966     @develprojects.sort! { |x,y| x.downcase <=> y.downcase }.uniq!
967     @develprojects.insert(0, all_packages)
968     @develprojects.insert(1, no_project)
969
970     @packages.sort! { |x,y| x['name'] <=> y['name'] }
971   end
972
973   private
974
975   def get_important_projects
976     predicate = "[attribute/@name='OBS:VeryImportantProject']"
977     return find_cached Collection, :id, :what => "project", :predicate => predicate
978   end
979
980
981   def filter_packages( project, filterstring )
982     result = Collection.find :id, :what => "package",
983       :predicate => "@project='#{project}' and contains(@name,'#{filterstring}')"
984     return result.each.map {|x| x.name}
985   end
986
987   def require_project
988     if !valid_project_name? params[:project]
989       unless request.xhr?
990         flash[:error] = "#{params[:project]} is not a valid project name"
991         redirect_to :controller => "project", :action => "list_public", :nextstatus => 404 and return
992       else
993         render :text => 'Not a valid project name', :status => 404 and return
994       end
995     end
996     @project = find_cached(Project, params[:project], :expires_in => 5.minutes )
997     check_user
998     unless @project
999       if @user and params[:project] == "home:#{@user}"
1000         # checks if the user is registered yet
1001         flash[:note] = "Your home project doesn't exist yet. You can create it now by entering some" +
1002           " descriptive data and press the 'Create Project' button."
1003         redirect_to :action => :new, :project => "home:" + session[:login] and return
1004       end
1005       # remove automatically if a user watches a removed project
1006       if @user and @user.watches? params[:project]
1007         @user.remove_watched_project params[:project] and @user.save
1008       end
1009       unless request.xhr?
1010         flash[:error] = "Project not found: #{params[:project]}"
1011         redirect_to :controller => "project", :action => "list_public", :nextstatus => 404 and return
1012       else
1013         render :text => "Project not found: #{params[:project]}", :status => 404 and return
1014       end
1015     end
1016   end
1017
1018   def require_prjconf
1019     begin
1020       @config = frontend.get_source(:project => params[:project], :filename => '_config')
1021     rescue ActiveXML::Transport::NotFoundError
1022       flash[:error] = "Project _config not found: #{params[:project]}"
1023       redirect_to :controller => "project", :action => "list_public", :nextstatus => 404
1024     end
1025   end
1026
1027   def require_meta
1028     begin
1029       @meta = frontend.get_source(:project => params[:project], :filename => '_meta')
1030     rescue ActiveXML::Transport::NotFoundError
1031       flash[:error] = "Project _meta not found: #{params[:project]}"
1032       redirect_to :controller => "project", :action => "list_public", :nextstatus => 404
1033     end
1034   end
1035
1036   def load_current_requests
1037     predicate = "state/@name='new' and action/target/@project='#{@project}'"
1038     @current_requests = Array.new
1039     coll = find_cached(Collection, :what => :request, :predicate => predicate, :expires_in => 1.minutes)
1040     coll.each_request do |req|
1041       @current_requests << req
1042     end
1043     @project_has_requests = !@current_requests.blank?
1044   end
1045
1046 end