diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b735b76d0cc..97b8171c8ed 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-06-11 09:56:13 +0000 using RuboCop version 0.57.1. +# on 2018-06-18 11:27:38 +0000 using RuboCop version 0.57.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -20,6 +20,41 @@ FactoryBot/DynamicAttributeDefinedStatically: - 'src/api/spec/factories/issue.rb' - 'src/api/spec/factories/issue_tracker.rb' +# Offense count: 3 +# Cop supports --auto-correct. +Layout/AlignArray: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/AlignHash: + Exclude: + - 'src/api/app/controllers/webui/obs_factory/distributions_controller.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_parameter, with_fixed_indentation +Layout/AlignParameters: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + +# Offense count: 13 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth. +# SupportedStyles: case, end +Layout/CaseIndentation: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + # Offense count: 6 # Cop supports --auto-correct. Layout/ClosingHeredocIndentation: @@ -55,11 +90,49 @@ Layout/EmptyComment: - 'src/api/app/models/bs_request_action_set_bugowner.rb' - 'src/api/config/routes.rb' -# Offense count: 3 +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AllowAdjacentOneLineDefs, NumberOfEmptyLines. +Layout/EmptyLineBetweenDefs: + Exclude: + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' + +# Offense count: 5 # Cop supports --auto-correct. Layout/EmptyLines: Exclude: - 'dist/obs_mirror_project' + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' + +# Offense count: 12 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only +Layout/EmptyLinesAroundClassBody: + Exclude: + - 'src/api/app/models/obs_factory/distribution_strategy_casp.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_factory_ppc.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_opensuse.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_sle12_sp1.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_sle15.rb' + - 'src/api/app/models/obs_factory/obs_project.rb' + - 'src/api/app/models/obs_factory/openqa_api.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines +Layout/EmptyLinesAroundModuleBody: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_casp.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_factory_ppc.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_opensuse.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_sle12_sp1.rb' + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' # Offense count: 4 # Cop supports --auto-correct. @@ -78,25 +151,55 @@ Layout/IndentHeredoc: Exclude: - 'dist/openQA_mail_notification.rb' -# Offense count: 2 +# Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: Width, IgnoredPatterns. Layout/IndentationWidth: Exclude: - 'dist/openQA_mail_notification.rb' - 'dist/t/spec/spec_helper.rb' + - 'src/api/app/helpers/webui/obs_factory/application_helper.rb' # Offense count: 19 # Cop supports --auto-correct. Layout/LeadingBlankLines: Enabled: false +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineMethodCallBraceLayout: + Exclude: + - 'src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Layout/SpaceAfterComma: + Exclude: + - 'src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb' + - 'src/api/app/models/obs_factory/distribution.rb' + # Offense count: 1 # Cop supports --auto-correct. Layout/SpaceAfterMethodName: Exclude: - 'dist/obs_mirror_project' +# Offense count: 2 +# Cop supports --auto-correct. +Layout/SpaceAfterNot: + Exclude: + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: space, no_space +Layout/SpaceAroundEqualsInParameterDefault: + Exclude: + - 'src/api/app/models/obs_factory/staging_project.rb' + # Offense count: 5 # Cop supports --auto-correct. Layout/SpaceAroundKeyword: @@ -110,13 +213,43 @@ Layout/SpaceAroundOperators: Exclude: - 'dist/obs_mirror_project' -# Offense count: 15 +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBrackets: space, no_space +Layout/SpaceInsideArrayLiteralBrackets: + Exclude: + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 10 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideBlockBraces: + Exclude: + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/models/obs_factory/request.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideHashLiteralBraces: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' + +# Offense count: 16 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: space, no_space Layout/SpaceInsideParens: Exclude: - 'dist/obs_mirror_project' + - 'src/api/app/models/obs_factory/distribution_strategy_casp.rb' # Offense count: 23 # Cop supports --auto-correct. @@ -125,11 +258,20 @@ Layout/Tab: Exclude: - 'src/api/db/migrate/20170123115500_remove_duplicate_indexes.rb' -# Offense count: 1 +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'src/api/app/models/obs_factory/distribution_strategy_sle12_sp1.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 2 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Exclude: - 'dist/clouduploader.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' # Offense count: 3 Lint/DuplicateMethods: @@ -141,6 +283,14 @@ Lint/DuplicateMethods: Lint/HandleExceptions: Enabled: false +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: runtime_error, standard_error +Lint/InheritException: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + # Offense count: 9 # Configuration parameters: MaximumRangeSize. Lint/MissingCopEnableDirective: @@ -188,39 +338,49 @@ Lint/UnneededCopEnableDirective: Exclude: - 'src/api/test/unit/schema_test.rb' +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'src/api/app/controllers/webui/obs_factory/application_controller.rb' + # Offense count: 40 Lint/UriEscapeUnescape: Enabled: false -# Offense count: 1 +# Offense count: 5 Lint/UselessAssignment: Exclude: - 'dist/obs_mirror_project' + - 'src/api/app/models/obs_factory/distribution_strategy_casp.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_factory.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_opensuse.rb' -# Offense count: 905 +# Offense count: 916 Metrics/AbcSize: Max: 239 -# Offense count: 486 +# Offense count: 488 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: Max: 505 -# Offense count: 24 +# Offense count: 26 # Configuration parameters: CountBlocks. Metrics/BlockNesting: Max: 6 -# Offense count: 90 +# Offense count: 93 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 1097 -# Offense count: 245 +# Offense count: 252 Metrics/CyclomaticComplexity: Max: 55 -# Offense count: 957 +# Offense count: 973 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 264 @@ -235,7 +395,7 @@ Metrics/ModuleLength: Metrics/ParameterLists: Max: 9 -# Offense count: 210 +# Offense count: 214 Metrics/PerceivedComplexity: Max: 55 @@ -256,6 +416,11 @@ Naming/AccessorMethodName: - 'src/api/app/models/user.rb' - 'src/api/test/functional/search_controller_test.rb' +# Offense count: 2 +Naming/BinaryOperatorParameterName: + Exclude: + - 'src/api/app/models/obs_factory/request.rb' + # Offense count: 1 Naming/ConstantName: Exclude: @@ -275,9 +440,10 @@ Naming/HeredocDelimiterNaming: - 'src/api/test/unit/project_test.rb' - 'src/api/test/unit/validator_test.rb' -# Offense count: 2 +# Offense count: 3 Naming/MemoizedInstanceVariableName: Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' - 'src/api/lib/activexml/transport.rb' - 'src/api/lib/authenticator.rb' @@ -339,6 +505,13 @@ Performance/InefficientHashSearch: Exclude: - 'src/api/app/models/user.rb' +# Offense count: 2 +# Cop supports --auto-correct. +Performance/RegexpMatch: + Exclude: + - 'src/api/app/models/obs_factory/distribution_strategy_factory.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + # Offense count: 49 RSpec/AnyInstance: Enabled: false @@ -354,7 +527,7 @@ RSpec/BeforeAfterAll: - 'dist/t/spec/features/0040_package_spec.rb' - 'src/api/spec/models/relationship_spec.rb' -# Offense count: 476 +# Offense count: 478 # Configuration parameters: Prefixes. # Prefixes: when, with, without RSpec/ContextWording: @@ -379,7 +552,7 @@ RSpec/DescribeClass: RSpec/DescribedClass: Enabled: false -# Offense count: 55 +# Offense count: 56 RSpec/EmptyLineAfterFinalLet: Enabled: false @@ -511,7 +684,7 @@ RSpec/IteratedExpectation: - 'src/api/spec/models/user_spec.rb' - 'src/api/spec/script/db_checker.rb' -# Offense count: 88 +# Offense count: 118 RSpec/LeadingSubject: Enabled: false @@ -653,6 +826,15 @@ Rails/CreateTableWithTimestamps: - 'src/api/db/migrate/20160321105300_request_counter.rb' - 'src/api/db/migrate/20171030143054_create_kiwi_preference_types.rb' +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: EnforceForPrefixed. +Rails/Delegate: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/obs_project.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + # Offense count: 85 # Configuration parameters: EnforcedStyle. # SupportedStyles: slashes, arguments @@ -708,6 +890,13 @@ Rails/Presence: - 'src/api/app/helpers/webui/package_helper.rb' - 'src/api/app/models/kiwi/image/xml_parser.rb' +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: NotNilAndNotEmpty, NotBlank, UnlessBlank. +Rails/Present: + Exclude: + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: Include. @@ -744,39 +933,47 @@ Rails/SkipsModelValidations: Rails/TimeZone: Enabled: false -# Offense count: 27 +# Offense count: 2 +Security/Open: + Exclude: + - 'src/api/app/models/obs_factory/distribution_strategy_factory.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_opensuse.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Security/YAMLLoad: + Exclude: + - 'src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 28 # Configuration parameters: EnforcedStyle. # SupportedStyles: inline, group Style/AccessModifierDeclarations: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, conditionals +Style/AndOr: Exclude: - - 'src/api/app/controllers/application_controller.rb' - - 'src/api/app/controllers/person_controller.rb' - - 'src/api/app/controllers/source_controller.rb' - - 'src/api/app/controllers/webui/package_controller.rb' - - 'src/api/app/models/flag.rb' - - 'src/api/app/models/group.rb' - - 'src/api/app/models/package_meta_file.rb' - - 'src/api/app/models/package_service_error_file.rb' - - 'src/api/app/models/project.rb' - - 'src/api/app/models/project_config_file.rb' - - 'src/api/app/models/project_meta_file.rb' - - 'src/api/lib/activexml/node.rb' - - 'src/api/test/functional/product_test.rb' - - 'src/api/test/functional/read_permission_test.rb' - - 'src/api/test/functional/source_controller_test.rb' + - 'src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb' + - 'src/api/app/models/obs_factory/openqa_api.rb' -# Offense count: 70 +# Offense count: 74 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: Enabled: false -# Offense count: 35 +# Offense count: 36 Style/ClassVars: Exclude: - 'src/api/app/controllers/test_controller.rb' - 'src/api/app/models/issue_tracker.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' - 'src/api/app/models/user.rb' - 'src/api/app/models/user_ldap_strategy.rb' - 'src/api/lib/activexml/node.rb' @@ -806,7 +1003,7 @@ Style/CommentedKeyword: - 'src/api/test/unit/user_ldap_strategy_test.rb' - 'src/api/test/unit/user_test.rb' -# Offense count: 90 +# Offense count: 93 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition @@ -823,6 +1020,28 @@ Style/DateTime: - 'src/api/spec/features/webui/requests_spec.rb' - 'src/api/test/functional/maintenance_test.rb' +# Offense count: 1 +Style/DoubleNegation: + Exclude: + - 'src/api/app/models/obs_factory/openqa_job.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty, nil, both +Style/EmptyElse: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Style/EmptyLiteral: + Exclude: + - 'src/api/app/models/obs_factory/openqa_api.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + # Offense count: 5 # Cop supports --auto-correct. Style/ExpandPathArguments: @@ -855,45 +1074,65 @@ Style/FormatStringToken: - 'src/api/app/helpers/webui/project_helper.rb' - 'src/api/lib/backend/connection_helper.rb' -# Offense count: 1007 +# Offense count: 1030 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: when_needed, always, never Style/FrozenStringLiteralComment: Enabled: false +# Offense count: 8 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'src/api/app/controllers/webui/obs_factory/distributions_controller.rb' + - 'src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys +Style/HashSyntax: + Exclude: + - 'src/api/app/models/obs_factory/request.rb' + # Offense count: 6 Style/IdenticalConditionalBranches: Exclude: - 'src/api/app/controllers/build_controller.rb' - 'src/api/app/controllers/webui/search_controller.rb' -# Offense count: 235 +# Offense count: 252 # Cop supports --auto-correct. Style/IfUnlessModifier: Enabled: false -# Offense count: 11 +# Offense count: 14 # Cop supports --auto-correct. # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: + - 'src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb' - 'src/api/app/controllers/webui/project_controller.rb' - 'src/api/app/mixins/has_attributes.rb' - 'src/api/app/models/bs_request.rb' - 'src/api/app/models/bs_request_action_submit.rb' - 'src/api/app/models/bs_request_permission_check.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' - 'src/api/app/models/package.rb' - 'src/api/app/models/project.rb' - 'src/api/app/views/source/_common_issues.xml.builder' - 'src/api/test/functional/request_events_test.rb' -# Offense count: 1 +# Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. Style/MethodCallWithoutArgsParentheses: Exclude: - 'dist/obs_mirror_project' + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' # Offense count: 1 Style/MethodMissingSuper: @@ -932,12 +1171,15 @@ Style/MultipleComparison: - 'src/api/lib/tasks/extract.rake' - 'src/api/script/reformat_memprof' -# Offense count: 9 +# Offense count: 16 # Cop supports --auto-correct. Style/MutableConstant: Exclude: - 'dist/openQA_mail_notification.rb' - 'dist/t/spec/spec_helper.rb' + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/request.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' # Offense count: 7 # Cop supports --auto-correct. @@ -947,28 +1189,48 @@ Style/NegatedIf: Exclude: - 'dist/obs_mirror_project' -# Offense count: 1 +# Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength. # SupportedStyles: skip_modifier_ifs, always Style/Next: Exclude: - 'dist/openQA_mail_notification.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: Strict. +Style/NumericLiterals: + MinDigits: 6 -# Offense count: 43 +# Offense count: 48 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false +# Offense count: 20 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/obs_project.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/models/obs_factory/request.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + # Offense count: 1 # Cop supports --auto-correct. Style/PerlBackrefs: Exclude: - 'dist/clouduploader.rb' -# Offense count: 19 +# Offense count: 20 # Cop supports --auto-correct. Style/RedundantBegin: Exclude: @@ -980,19 +1242,33 @@ Style/RedundantBegin: - 'src/api/app/jobs/consistency_check_job.rb' - 'src/api/app/mixins/build_log_support.rb' - 'src/api/app/models/distribution.rb' + - 'src/api/app/models/obs_factory/distribution.rb' - 'src/api/app/models/package.rb' - 'src/api/app/models/project.rb' - 'src/api/db/checker.rb' - 'src/api/script/start_test_backend' -# Offense count: 91 +# Offense count: 6 +# Cop supports --auto-correct. +Style/RedundantSelf: + Exclude: + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' + +# Offense count: 92 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Enabled: false -# Offense count: 25 +# Offense count: 3 +# Cop supports --auto-correct. +Style/RescueModifier: + Exclude: + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/models/obs_factory/request.rb' + +# Offense count: 26 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, explicit @@ -1012,21 +1288,12 @@ Style/StderrPuts: Exclude: - 'dist/openQA_mail_notification.rb' -# Offense count: 70 +# Offense count: 95 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: - Exclude: - - 'dist/clouduploader.rb' - - 'dist/obs_mirror_project' - - 'dist/openQA_mail_notification.rb' - - 'dist/t/spec/features/0010_authentication_spec.rb' - - 'dist/t/spec/features/0020_interconnect_spec.rb' - - 'dist/t/spec/features/0030_project_spec.rb' - - 'dist/t/spec/features/0040_package_spec.rb' - - 'dist/t/spec/spec_helper.rb' - - 'dist/t/spec/support/capybara.rb' + Enabled: false # Offense count: 1 # Cop supports --auto-correct. @@ -1036,6 +1303,14 @@ Style/StringLiteralsInInterpolation: Exclude: - 'dist/clouduploader.rb' +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: IgnoredMethods. +# IgnoredMethods: respond_to, define_method +Style/SymbolProc: + Exclude: + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + # Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. @@ -1045,6 +1320,12 @@ Style/TrivialAccessors: - 'src/api/lib/activexml/node.rb' - 'src/api/test/test_helper.rb' +# Offense count: 1 +# Cop supports --auto-correct. +Style/UnlessElse: + Exclude: + - 'src/api/app/models/obs_factory/openqa_job.rb' + # Offense count: 2 # Cop supports --auto-correct. Style/UnneededCondition: @@ -1057,3 +1338,24 @@ Style/UnneededCondition: Style/UnneededInterpolation: Exclude: - 'dist/obs_mirror_project' + +# Offense count: 21 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + Exclude: + - 'src/api/app/models/obs_factory/distribution.rb' + - 'src/api/app/models/obs_factory/distribution_strategy_factory.rb' + - 'src/api/app/models/obs_factory/obs_project.rb' + - 'src/api/app/models/obs_factory/openqa_job.rb' + - 'src/api/app/models/obs_factory/request.rb' + - 'src/api/app/models/obs_factory/staging_project.rb' + - 'src/api/app/presenters/obs_factory/obs_project_presenter.rb' + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/ZeroLengthPredicate: + Exclude: + - 'src/api/app/presenters/obs_factory/staging_project_presenter.rb' diff --git a/src/api/app/assets/stylesheets/webui/obs_factory/application.css b/src/api/app/assets/stylesheets/webui/obs_factory/application.css new file mode 100644 index 00000000000..ac5079793e8 --- /dev/null +++ b/src/api/app/assets/stylesheets/webui/obs_factory/application.css @@ -0,0 +1,425 @@ +/* Staging projects dashboard table */ +.staging-dashboard th.staging-project { + text-align: right; +} + +.staging-dashboard .packages-list { + text-align: left; + margin: 6px; +} + +.staging-dashboard td { + text-align: center +} + +.staging-dashboard td img { + margin-top: 4px +} + +/* Fix an issue with rowspans */ +.staging-dashboard tr:last-child td { + border-bottom: 1px dotted #CCCCCC +} + +/* openQA */ +a.openqa-passed { + color: #6eb927 +} + +a.openqa-failed { + color: red +} + +a.openqa-none { + color: #006699 +} + +a.openqa-incomplete { + color: black +} + +/* Proper formating for dl, dt and dd */ +.content-wrapper .box .factory-summary dl { + margin-left: 10px; + margin-right: 10px; +} + +.content-wrapper .box .factory-summary dt { + float: left; + clear: left; + width: 160px; + text-align: right; + font-weight: bold; +} + +.content-wrapper .box .factory-summary dt img { + float: left +} + +.content-wrapper .box .factory-summary dt:after { + content: ":" +} + +.content-wrapper .box .factory-summary dd { + margin: 0 0 0 170px; + padding: 0 0 1em 0; +} + +/* Avoid excessive margin in nested ul */ +.content-wrapper .box .factory-summary dd ul { + margin: 0 0 0 0; +} + +.content-wrapper .box .factory-summary dd ul li { + margin-left: 0; +} + +/* More compact h3 */ +.content-wrapper .box .factory-summary h3 { + margin-top: 0; + margin-bottom: 0.5em +} + +.staging-dashboard .request { + white-space: nowrap; + margin: 1ex; + padding: 3px; +} + +.staging-dashboard .request a.staging_expand, +.staging-dashboard .request a.staging_collapse { + color: #069; +} + +.staging-dashboard div.staging_collapsible { + display: inline-block; +} + +.staging-dashboard .ok { + background-color: #53b25f; +} + +.color-legend .ok { + background-color: #53b25f; +} + +.staging-dashboard .review { + background-color: #d9b200; +} + +.color-legend .review { + background-color: #d9b200; +} + +.staging-dashboard .obsolete { + background-color: #42a2cc; +} + +.color-legend .obsolete { + background-color: #42a2cc; +} + +.staging-dashboard .untracked { + background-color: #666; +} + +.color-legend .untracked { + background-color: #666; +} + +.staging-dashboard .request a { + color: white; + margin: 0; + line-height: 1em; +} + +.staging-dashboard .packages-list { + text-align: left; + list-style-type: none; +} + +.staging-dashboard .request { + padding-left: 2px; + padding-right: 3px; + margin-right: .5ex; + border-radius: 3px; + float: left; +} + +.staging-dashboard tr:nth-child(odd) { + background-color: #fff; +} + +.staging-dashboard tr:nth-child(even) { + background-color: #eee; +} + +.staging-dashboard ul.problem_list { + white-space: nowrap; + margin-left: 0px; + text-align: left; + margin-top: 0px; + list-style-type: none; +} + +.staging-dashboard ul.problem_list li { + margin-left: 8px; + line-height: 170%; +} + +.staging-dashboard .delete a { + text-decoration: line-through; +} + +.color-legend .delete { + background-color: #FFFFFF; +} + +.overall-state.staging-project { + border-radius: 8px; + margin: 5px; + padding: 4px; + text-align: center; + min-width: 5em; +} + +.staging-project.state-building { + background-color: #cca66a; +} + +.staging-project.state-review { + background-color: #6a98cc; +} + +.staging-project.state-testing { + background-color: #6ab4cc; +} + +.staging-project.state-unacceptable { + background-color: #333; +} + +.staging-project.state-failed { + background-color: #ff4e46; +} + +.staging-project.state-acceptable { + background-color: #75c195; +} + +.staging-project .letter { + font-size: 150%; + margin: 5px; + padding: 16px; + background-color: #fff; + border-radius: 26px; + box-shadow: 0px -2px 2px #999; +} + +.staging-project .state a { + color: white; +} + +.staging-project .splitter-info { + margin-top: 5px; + background-color: white; + border-radius: 5px; + box-shadow: 0px -2px 2px #999; + cursor: default; +} + +.staging-project .splitter-info div { + overflow: hidden; + text-overflow: ellipsis; + max-width: 7em; +} + +.staging-project .splitter-info div:hover { + overflow: visible; + max-width: none; +} + +ul.color-legend { + list-style-type: none; + margin-left: 4px; + margin-right: 10px; + margin-top: 0.5em; +} + +#legends p { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +ul.color-legend li { + margin-left: 8px; + vertical-align: middle; +} + +li.delete { + text-decoration: line-through; +} + +.color-legend span { + vertical-align: top; + border: 1px #ccc solid; + min-width: 15px; + min-height: 15px; + display: inline-block; + margin-right: 8px; +} + +.color-legend .fa { + min-width: 15px; + min-height: 15px; + display: inline-block; + margin-right: 8px; +} + +.staging_backlog li, +.staging_backlog_ignored li { + margin-left: 8px; +} + +.ok-checkbox { + color: #53B25F; +} + +.fa.missing-review { + color: white; +} + +#main-dashboard .overall-state { + float: left; + min-height: 8em; +} + +ul#main-dashboard { + list-style-type: none +} + +ul.project-results { + list-style-type: none +} + +.project-results a { + color: white; +} + +.project-results li { + float: left +} + +.project-results li.project-result-succeeded { + background-color: #53b25f +} + +.project-results li.project-result-building { + background-color: #cca66a +} + +.project-results li.project-result-failed { + background-color: #ff4e46 +} + +.nav{ + float : left; + width : 100%; +} + +.nav ul { + list-style-type: none; + text-align: center; + padding: 0; + margin: 8px; +} + +.nav a { + text-decoration: none; + display: inline-block; +} + +.nav > input{ + display : none; +} + +.nav > section > h2{ + float : left; + box-sizing : border-box; + overflow : hidden; + padding : 0.3em 0.25em 0; + font-size : 1em; +} + +.nav > input:first-child + section > h2{ + padding-left : 1em; +} + +.nav > section > h2 > label{ + display : block; + padding : 0.25em 0.75em; + border : 1px solid #ccc; + border-bottom : none; + border-top-left-radius : 5px; + border-top-right-radius : 5px; + background : #fff; + cursor : pointer; +} + +.nav > section > div{ + position : relative; + z-index : 1; + float : right; + box-sizing : border-box; + width : 100%; + margin : 2.5em 0 0 -100%; + background : #fff; +} + +.nav > input:checked + section > h2{ + position : relative; + z-index : 2; +} + +.nav > input:not(:checked) + section > div{ + display : none; +} + +div.factory-summary dt:nth-of-type(even), div.factory-summary dd:nth-of-type(even) { + background: #eee; +} + +#staging-detail .request { + padding-left: 2px; + padding-right: 3px; + margin: 3px; + border-radius: 3px; + float: left; + list-style-type: none; +} + +#staging-detail .request a { + color: white; + margin: 0; + line-height: 1em; +} + +#staging-detail .ok { + background-color: #53b25f; +} + +#staging-detail .review { + background-color: #d9b200; +} + +#staging-detail .obsolete { + background-color: #42a2cc; +} + +#staging-detail .untracked { + background-color: #666; +} + +#staging-detail .delete a { + text-decoration: line-through; +} diff --git a/src/api/app/controllers/webui/obs_factory/application_controller.rb b/src/api/app/controllers/webui/obs_factory/application_controller.rb new file mode 100644 index 00000000000..c0c7db448e3 --- /dev/null +++ b/src/api/app/controllers/webui/obs_factory/application_controller.rb @@ -0,0 +1,9 @@ +module Webui::ObsFactory + class ApplicationController < ::Webui::WebuiController + layout 'webui/obs_factory/application' + + rescue_from ::ObsFactory::OpenqaApi::OpenqaFailure do |ex| + render text: "failure in openQA" + end + end +end diff --git a/src/api/app/controllers/webui/obs_factory/distributions_controller.rb b/src/api/app/controllers/webui/obs_factory/distributions_controller.rb new file mode 100644 index 00000000000..f13c1e40aea --- /dev/null +++ b/src/api/app/controllers/webui/obs_factory/distributions_controller.rb @@ -0,0 +1,49 @@ +module Webui::ObsFactory + class DistributionsController < ApplicationController + respond_to :html + + before_action :require_distribution, :require_dashboard + + def show + @staging_projects = ::ObsFactory::StagingProjectPresenter.sort(@distribution.staging_projects) + @versions = { source: @distribution.source_version, + totest: @distribution.totest_version, + published: @distribution.published_version } + @ring_prjs = ::ObsFactory::ObsProjectPresenter.wrap(@distribution.ring_projects) + @standard = ::ObsFactory::ObsProjectPresenter.new(@distribution.standard_project) + @live = @distribution.live_project + @live = ::ObsFactory::ObsProjectPresenter.new(@live) unless @live.nil? + @images = ::ObsFactory::ObsProjectPresenter.new(@distribution.images_project) + @openqa_jobs = @distribution.openqa_jobs_for(:totest) + calculate_reviews + # For the breadcrumbs + @project = @distribution.project + end + + protected + + def calculate_reviews + @reviews = {} + @reviews[:review_team] = @distribution.requests_with_reviews_for_group('opensuse-review-team').size + @reviews[:factory_auto] = @distribution.requests_with_reviews_for_group('factory-auto').size + @reviews[:legal_auto] = @distribution.requests_with_reviews_for_group('legal-auto').size + @reviews[:legal_team] = @distribution.requests_with_reviews_for_group('legal-team').size + @reviews[:repo_checker] = @distribution.requests_with_reviews_for_user('repo-checker').size + end + + private + + def require_distribution + @distribution = ::ObsFactory::Distribution.find(params[:project]) + unless @distribution + redirect_to main_app.root_path, flash: { error: "#{params[:project]} is not a valid openSUSE distribution, can't offer dashboard" } + end + end + + def require_dashboard + if @distribution.staging_projects.empty? + redirect_to main_app.root_path, flash: { error: "#{params[:project]} does not offer a dashboard" } + end + end + end +end diff --git a/src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb b/src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb new file mode 100644 index 00000000000..0bd2e6f7843 --- /dev/null +++ b/src/api/app/controllers/webui/obs_factory/staging_projects_controller.rb @@ -0,0 +1,67 @@ +module Webui::ObsFactory + class StagingProjectsController < ApplicationController + respond_to :json, :html + + before_action :require_distribution + before_action :require_project_name, only: [:show] + + def index + respond_to do |format| + format.html do + @staging_projects = ::ObsFactory::StagingProjectPresenter.sort(@distribution.staging_projects_all) + @backlog_requests = ::ObsFactory::Request.with_open_reviews_for(by_group: @distribution.staging_manager, target_project: @distribution.name) + @requests_state_new = ::ObsFactory::Request.in_state_new(by_group: @distribution.staging_manager, target_project: @distribution.name) + + staging_project = Project.find_by_name("#{@distribution.project}:Staging") + dashboard_package = Package.find_by_project_and_name(staging_project.name, 'dashboard') + + if dashboard_package && dashboard_package.file_exists?('ignored_requests') + file = ::Backend::Api::Sources::Package.file(staging_project.name, dashboard_package.name, 'ignored_requests') + @ignored_requests = YAML.load(file) + end + + if @ignored_requests + @backlog_requests_ignored = @backlog_requests.select { |req| @ignored_requests.key?(req.number) } + @backlog_requests = @backlog_requests.select { |req| !@ignored_requests.key?(req.number) } + @requests_state_new = @requests_state_new.select { |req| !@ignored_requests.key?(req.number) } + @backlog_requests_ignored.sort! { |x,y| x.package <=> y.package } + else + @backlog_requests_ignored = [] + end + @backlog_requests.sort! { |x,y| x.package <=> y.package } + @requests_state_new.sort! { |x,y| x.package <=> y.package } + # For the breadcrumbs + @project = @distribution.project + end + format.json { render json: @distribution.staging_projects_all } + end + end + + def show + respond_to do |format| + format.html do + @staging_project = ::ObsFactory::StagingProjectPresenter.new(@staging_project) + # For the breadcrumbs + @project = @distribution.project + end + format.json { render json: @staging_project } + end + end + + private + + def require_distribution + @distribution = ::ObsFactory::Distribution.find(params[:project]) + unless @distribution + redirect_to main_app.root_path, flash: { error: "#{params[:project]} is not a valid openSUSE distribution, can't offer dashboard" } + end + end + + def require_project_name + @staging_project = ::ObsFactory::StagingProject.find(@distribution, params[:project_name]) + unless @staging_project + redirect_to main_app.root_path, flash: { error: "#{params[:project_name]} is not a valid staging project" } + end + end + end +end diff --git a/src/api/app/helpers/webui/obs_factory/application_helper.rb b/src/api/app/helpers/webui/obs_factory/application_helper.rb new file mode 100644 index 00000000000..9459f6249bd --- /dev/null +++ b/src/api/app/helpers/webui/obs_factory/application_helper.rb @@ -0,0 +1,5 @@ +module Webui::ObsFactory::ApplicationHelper + def openqa_links_helper + ObsFactory::OpenqaJob.openqa_links_url + end +end diff --git a/src/api/app/models/obs_factory/distribution.rb b/src/api/app/models/obs_factory/distribution.rb new file mode 100644 index 00000000000..8f878590f40 --- /dev/null +++ b/src/api/app/models/obs_factory/distribution.rb @@ -0,0 +1,224 @@ +require 'open-uri' + +module ObsFactory + + class UnknownDistribution < Exception + end + + # A Distribution. Contains a reference to the corresponding Project object. + class Distribution + include ActiveModel::Model + extend ActiveModel::Naming + extend Forwardable + + SOURCE_VERSION_FILE = { package_name: '000product', filename: 'openSUSE.product' } + RINGS_PREFIX = ":Rings" + + attr_accessor :project, :strategy + + def initialize(project = nil) + self.project = project + self.strategy = distribution_strategy_for_project(project) + end + + def self.attributes + %w(name description staging_projects openqa_version openqa_group + source_version totest_version published_version staging_manager + standard_project live_project images_project ring_projects) + end + + def attributes + Hash[self.class.attributes.map { |a| [a, nil] }] + end + + def_delegators :@strategy, :root_project_name, :url_suffix, :openqa_version, + :openqa_iso, :arch, :openqa_group, :staging_manager + + # Find a distribution by id + # + # @return [Distribution] the distribution + def self.find(id) + project = ::Project.find_by_name(id) + if project + begin + Distribution.new(project) + rescue UnknownDistribution + nil + end + else + nil + end + end + + # Name of the associated project + # + # @return [String] name of the Project object + def name + project.name + end + + # Id of the distribution + # + # @return [String] name + def id + name + end + + # Description of the associated project + # + # @return [String] description of the Project object + def description + project.description + end + + # Version of the distribution used as ToTest + # + # @return [String] version string + def totest_version + Rails.cache.fetch("totest_version_for_#{name}", expires_in: 10.minutes) do + strategy.totest_version + end + end + + # Version of the published distribution + # + # @return [String] version string + def published_version + Rails.cache.fetch("published_version_for_#{name}", expires_in: 10.minutes) do + strategy.published_version + end + end + + # Staging projects associated to the distribution + # + # @return [Array] array of StagingProject objects + def staging_projects + @staging_projects ||= StagingProject.for(self) + end + + # Staging projects associated to the distribution, including non-letter + # + # @return [Array] array of StagingProject objects + def staging_projects_all + @staging_projects ||= StagingProject.for(self, false) + end + + # Version of the distribution used as source + # + # @return [String] version string + def source_version + Rails.cache.fetch("source_version_for_#{name}", expires_in: 10.minutes) do + begin + p = Xmlhash.parse(Backend::Api::Sources::Package.file(name, SOURCE_VERSION_FILE[:package_name], SOURCE_VERSION_FILE[:filename])) + p.get('products').get('product').get('version') + rescue ActiveXML::Transport::NotFoundError + nil + end + end + end + + # openQA jobs related with a given version of the distribution + # + # @param [#to_s] version must be :source, :totest or :published + # @return [Array] list of OpenqaJob objects + def openqa_jobs_for(version) + filter = {distri: 'opensuse', version: strategy.openqa_version, build: send(:"#{version}_version"), group: strategy.openqa_group} + OpenqaJob.find_all_by(filter, exclude_modules: true) + end + + # Requests with some open review targeting the distribution, filtered by + # the group in charge of the open reviews + # + # @param [String] group name of the group + # @return [Array] list of Request objects + def requests_with_reviews_for_group(group) + Request.with_open_reviews_for(by_group: group, target_project: root_project_name) + end + + # Requests with some open review targeting the distribution, filtered by + # the user in charge of the open reviews + # + # @param [String] user name of the user + # @return [Array] list of Request objects + def requests_with_reviews_for_user(user) + Request.with_open_reviews_for(by_user: user, target_project: root_project_name) + end + + # Standard project + # + # @return [ObsProject] standard + def standard_project + if @standard_project.nil? + @standard_project = ObsProject.new(name, 'standard') + @standard_project.exclusive_repository = 'standard' + end + @standard_project + end + + # Live project + # + # @return [ObsProject] live + def live_project + if @live_project.nil? + @live_project = ObsProject.new("#{name}:Live", 'live') + if @live_project.project.nil? + @live_project = nil + else + @live_project.exclusive_repository = 'images' + end + end + @live_project + end + + # Images project + # + # @return [ObsProject] images + def images_project + if @images_project.nil? + @images_project = ObsProject.new(name, 'images') + @images_project.exclusive_repository = 'images' + end + @images_project + end + + # Projects defining the distribution rings + # + # @return [Array] list of ObsProject objects nicknamed with numbers + def ring_projects + @ring_projects ||= strategy.rings.each_with_index.map do |r,idx| + ObsProject.new("#{rings_project_name}:#{idx}-#{r}", "#{idx}-#{r}") + end + end + + # Name of the project used as top-level for the ring projects + # + # @return [String] project name + def rings_project_name + "#{root_project_name}#{RINGS_PREFIX}" + end + + # URL parameter for openqa's /test route to be opened in view + # for given project + # + # @return [String] URL part (e.g. match=A) + def openqa_filter(project) + return strategy.openqa_filter(project) + end + + private + + def distribution_strategy_for_project(project) + s = case project.name + when 'openSUSE:Factory' then DistributionStrategyFactory.new + when 'openSUSE:Factory:PowerPC' then DistributionStrategyFactoryPPC.new + when /^openSUSE:.*/ then DistributionStrategyOpenSUSE.new + when /^SUSE:SLE-12-SP\d:GA/ then DistributionStrategySLE12SP1.new + when /^SUSE:SLE-15:GA/ then DistributionStrategySLE15.new + when /^SUSE:SLE-12-SP.*CASP\d*/ then DistributionStrategyCasp.new + else raise UnknownDistribution + end + s.project = project + s + end + end +end diff --git a/src/api/app/models/obs_factory/distribution_strategy_casp.rb b/src/api/app/models/obs_factory/distribution_strategy_casp.rb new file mode 100644 index 00000000000..353c7ed5ba6 --- /dev/null +++ b/src/api/app/models/obs_factory/distribution_strategy_casp.rb @@ -0,0 +1,50 @@ +module ObsFactory + + class DistributionStrategyCasp < DistributionStrategyFactory + + def casp_version + match = project.name.match(/^SUSE:SLE-12-SP.*CASP(\d*)/ ) + match[1] + end + + def staging_manager + 'caasp-staging-managers' + end + + def repo_url + 'http://download.opensuse.org/distribution/13.2/repo/oss/media.1/build' + end + + def openqa_version + '1.0' + end + + # Name of the ISO file by the given staging project tracked on openqa + # + # @return [String] file name + def openqa_iso(project) + project_iso(project) + end + + # Name of the ISO file produced by the given staging project's Test-DVD + # + # Not part of the Strategy API, but useful for subclasses + # + # @return [String] file name + def project_iso(project) + arch = self.arch + buildresult = Buildresult.find_hashed(project: project.name, package: "CAASP-dvd5-DVD-#{arch}", + repository: 'images', + view: 'binarylist') + binaries = [] + # we get multiple architectures, but only one with binaries + buildresult.elements('result') do |r| + r['binarylist'].elements('binary') do |b| + return b['filename'] if /\.iso$/ =~ b['filename'] + end + end + nil + end + + end +end diff --git a/src/api/app/models/obs_factory/distribution_strategy_factory.rb b/src/api/app/models/obs_factory/distribution_strategy_factory.rb new file mode 100644 index 00000000000..15a0f2c2f71 --- /dev/null +++ b/src/api/app/models/obs_factory/distribution_strategy_factory.rb @@ -0,0 +1,127 @@ +module ObsFactory + # this is not a Factory pattern, this is for openSUSE:Factory :/ + class DistributionStrategyFactory + attr_accessor :project + + # String to pass as version to filter the openQA jobs + # + # @return [String] version parameter + def openqa_version + 'Tumbleweed' + end + + def openqa_group + 'openSUSE Tumbleweed' + end + + # + # Name of the project used as top-level for the staging projects and + # the rings + # + # @return [String] project name + def root_project_name + project.name + end + + def test_dvd_prefix + 'Test-DVD' + end + + def totest_version_file + 'images/local/000product:openSUSE-cd-mini-x86_64' + end + + def arch + 'x86_64' + end + + def url_suffix + 'tumbleweed/iso' + end + + def rings + %w[Bootstrap MinimalX TestDVD] + end + + def repo_url + 'http://download.opensuse.org/tumbleweed/repo/oss/media.1/media' + end + + def published_arch + 'i586' + end + + # the prefix openQA gives test ISOs + # + # @return [String] e.g. 'openSUSE-Staging' + def openqa_iso_prefix + 'openSUSE-Staging' + end + + # Name of the ISO file by the given staging project tracked on openqa + # + # @return [String] file name + def openqa_iso(project) + iso = project_iso(project) + return nil if iso.nil? + ending = iso.gsub!(/.*-Build/, '') + suffix = /DVD$/ =~ project.name ? 'Staging2' : 'Staging' + openqa_iso_prefix + ":#{project.letter}-#{suffix}-DVD-#{arch}-Build#{ending}" + end + + # Name of the ISO file produced by the given staging project's Test-DVD + # + # Not part of the Strategy API, but useful for subclasses + # + # @return [String] file name + def project_iso(project) + arch = self.arch + buildresult = Buildresult.find_hashed(project: project.name, package: "#{test_dvd_prefix}-#{arch}", + repository: 'images', + view: 'binarylist') + binaries = [] + # we get multiple architectures, but only one with binaries + buildresult.elements('result') do |r| + r.get('binarylist').elements('binary') do |b| + return b['filename'] if /\.iso$/ =~ b['filename'] + end + end + nil + end + protected :project_iso + + def staging_manager + 'factory-staging' + end + + # Version of the distribution used as ToTest + # + # @return [String] version string + def totest_version + d = Xmlhash.parse(ActiveXML.backend.direct_http("/build/#{project.name}:ToTest/#{totest_version_file}")) + d.elements('binary') do |b| + matchdata = /.*(Snapshot|Build)(.*)-Media\.iso$/.match(b['filename']) + return matchdata[2] if matchdata + end + rescue + nil + end + + # Version of the published distribution + # + # @return [String] version string + def published_version + begin + f = open(repo_url) + rescue OpenURI::HTTPError => e + return 'unknown' + end + matchdata = /openSUSE-(.*)-#{published_arch}-.*/.match(f.read) + matchdata[1] + end + + def openqa_filter(project) + "match=Staging:#{project.letter}" + end + end +end diff --git a/src/api/app/models/obs_factory/distribution_strategy_factory_ppc.rb b/src/api/app/models/obs_factory/distribution_strategy_factory_ppc.rb new file mode 100644 index 00000000000..2970f1f4f51 --- /dev/null +++ b/src/api/app/models/obs_factory/distribution_strategy_factory_ppc.rb @@ -0,0 +1,34 @@ +module ObsFactory + + # PowerPC to Factory Diff + class DistributionStrategyFactoryPPC < DistributionStrategyFactory + + def root_project_name + "openSUSE:Factory" + end + + def totest_version_file + 'images/local/000product:openSUSE-cd-mini-ppc64le' + end + + def arch + 'ppc64le' + end + + def url_suffix + 'ports/ppc/factory' + end + + def openqa_group + 'openSUSE Tumbleweed PowerPC' + end + + def repo_url + 'http://download.opensuse.org/ports/ppc/factory/repo/oss/media.1/build' + end + + def published_arch + "ppc64le" + end + end +end diff --git a/src/api/app/models/obs_factory/distribution_strategy_opensuse.rb b/src/api/app/models/obs_factory/distribution_strategy_opensuse.rb new file mode 100644 index 00000000000..c6819530a25 --- /dev/null +++ b/src/api/app/models/obs_factory/distribution_strategy_opensuse.rb @@ -0,0 +1,66 @@ +module ObsFactory + + # this class tracks the differences between Factory and the upcoming release + class DistributionStrategyOpenSUSE < DistributionStrategyFactory + def opensuse_version + # Remove the "openSUSE:" part + project.name[9..-1] + end + + def opensuse_leap_version + # Remove the "openSUSE:Leap:" part + project.name[14..-1] + end + + def openqa_version + opensuse_leap_version + end + + def openqa_group + "openSUSE Leap #{opensuse_leap_version}" + end + + def repo_url + "http://download.opensuse.org/distribution/leap/#{opensuse_leap_version}/repo/oss/media.1/media" + end + + def url_suffix + "distribution/leap/#{opensuse_leap_version}/iso" + end + + def openqa_iso_prefix + "openSUSE-#{opensuse_version}-Staging" + end + + def published_arch + 'x86_64' + end + + def test_dvd_prefix + '000product:openSUSE-dvd5-dvd' + end + + def totest_version_file + 'images/local/000product:openSUSE-cd-mini-x86_64' + end + + # Version of the published distribution + # + # @return [String] version string + def published_version + begin + f = open(repo_url) + rescue OpenURI::HTTPError => e + return 'unknown' + end + matchdata = %r{openSUSE-#{opensuse_leap_version}-#{published_arch}-Build(.*)}.match(f.read) + matchdata[1] + end + + # URL parameter for Leap + def openqa_filter(project) + return "match=#{opensuse_leap_version}:S:#{project.letter}" + end + + end +end diff --git a/src/api/app/models/obs_factory/distribution_strategy_sle12_sp1.rb b/src/api/app/models/obs_factory/distribution_strategy_sle12_sp1.rb new file mode 100644 index 00000000000..a535ee7a35c --- /dev/null +++ b/src/api/app/models/obs_factory/distribution_strategy_sle12_sp1.rb @@ -0,0 +1,33 @@ +module ObsFactory + + class DistributionStrategySLE12SP1 < DistributionStrategyFactory + + def sp_version + match = project.name.match(/SLE-12-(.*):GA/) + match[1] + end + + def staging_manager + 'sle-staging-managers' + end + + def repo_url + 'http://download.opensuse.org/distribution/13.2/repo/oss/media.1/build' + end + + def openqa_version + "SLES 12 #{sp_version}" + end + + # Name of the ISO file by the given staging project tracked on openqa + # + # @return [String] file name + def openqa_iso(project) + ending = project_iso(project) + return if ending.nil? + ending.gsub!(/.*-Build/, '') + "SLE12-#{sp_version}-Staging:#{project.letter}-Test-Server-DVD-#{arch}-Build#{project.letter}.#{ending}" + end + + end +end diff --git a/src/api/app/models/obs_factory/distribution_strategy_sle15.rb b/src/api/app/models/obs_factory/distribution_strategy_sle15.rb new file mode 100644 index 00000000000..b7dcbe187c0 --- /dev/null +++ b/src/api/app/models/obs_factory/distribution_strategy_sle15.rb @@ -0,0 +1,30 @@ +module ObsFactory + class DistributionStrategySLE15 < DistributionStrategyFactory + def staging_manager + 'sle-staging-managers' + end + + def repo_url + 'http://download.opensuse.org/distribution/13.2/repo/oss/media.1/build' + end + + def openqa_version + 'SLES 15' + end + + def test_dvd_prefix + '000product:SLES-cd-DVD' + end + + # Name of the ISO file by the given staging project tracked on openqa + # + # @return [String] file name + def openqa_iso(project) + ending = project_iso(project) + return if ending.nil? + ending.gsub!(/.*-Build/, '') + "SLE-15-Staging:#{project.letter}-Installer-DVD-#{arch}-Build#{project.letter}.#{ending}" + end + + end +end diff --git a/src/api/app/models/obs_factory/obs_project.rb b/src/api/app/models/obs_factory/obs_project.rb new file mode 100644 index 00000000000..88189bb8663 --- /dev/null +++ b/src/api/app/models/obs_factory/obs_project.rb @@ -0,0 +1,56 @@ +module ObsFactory + # A wrapper around Project, containing a reference to the original Project + # object and adding some methods and attributes. + class ObsProject + + attr_accessor :project, :nickname, :exclusive_repository + + def initialize(name, nick) + self.project = Project.find_by_name(name) + self.nickname = nick + end + + # Name of the associated project + # + # @return [String] the name + def name + project.name + end + + # Repository names defined for the project + # + # @return [Array] list of names + def repos + ret = {} + build_summary.elements('result') do |r| + ret[r['repository']] = 1 + end + ret.keys + end + + # Hashed summary of the build results. + # + # Cached during 5 minutes from the backend. + # + # @return [XMLHash] summary + def build_summary + Rails.cache.fetch("build_summary_for_#{name}", expires_in: 5.minutes) do + ::Buildresult.find_hashed(project: name, view: 'summary') + end + end + + # Number of build failures in the project + # + # @return [Integer] failures count + def build_failures_count + buildresult = Buildresult.find_hashed(project: name, code: %w(failed broken unresolvable)) + bp = {} + buildresult.elements('result') do |result| + result.elements('status') do |s| + bp[s['package']] = 1 + end + end + bp.keys.count + end + end +end diff --git a/src/api/app/models/obs_factory/openqa_api.rb b/src/api/app/models/obs_factory/openqa_api.rb new file mode 100644 index 00000000000..ece534b039c --- /dev/null +++ b/src/api/app/models/obs_factory/openqa_api.rb @@ -0,0 +1,63 @@ +require 'net/http' + +# Commodity class to encapsulate calls to the openQA API. +module ObsFactory + class OpenqaApi + + class OpenqaFailure < APIException + setup 408 + end + + def initialize(base_url) + @base_url = base_url.chomp('/') + '/api/v1/' + end + + # A get that follows redirects - openqa redirects to https + def _get(uri, counter_redirects) + req_path = uri.path + req_path << "?" + uri.query unless uri.query.empty? + req = Net::HTTP::Get.new(req_path) + resp = Net::HTTP.start(uri.host, use_ssl: uri.scheme == "https") { |http| http.request(req) } + # Prevent endless loop in case response is always 301 or 302 + unless counter_redirects >= 5 + if resp.code.to_i == 302 or resp.code.to_i == 301 + counter_redirects += 1 + Rails.logger.debug "following to #{resp.header['location']}" + return _get(URI.parse(resp.header['location']), counter_redirects) + end + end + return resp + end + + # Performs a GET query on the openQA API + # + # @param [String] url action to call + # @param [Hash] params query parameters + # @param [Hash] options additional options. Right now :base_url to + # overwrite the default one + # @return [Object] the response decoded (usually a Hash) + def get(url, params = {}, options = {}) + # Check if params for GET request are completely to prevent overhead for openQA + # and timeouts for the dashboard + params.each do |key, value| + if value.nil? + Rails.logger.error "OpenQA API GET failure: Missing parameters for #{key}" + return Hash.new + end + end + + if options[:base_url] + uri = URI.join(options[:base_url].chomp('/') + '/', url) + else + uri = URI.join(@base_url, url) + end + uri.query = params.to_query + resp = _get(uri, 0) + unless resp.code.to_i == 200 + Rails.logger.error "OpenQA API GET failure: \"#{url}\" with \"#{params.to_query}\"" + return Hash.new + end + ActiveSupport::JSON.decode(resp.body) + end + end +end diff --git a/src/api/app/models/obs_factory/openqa_job.rb b/src/api/app/models/obs_factory/openqa_job.rb new file mode 100644 index 00000000000..c374387d40f --- /dev/null +++ b/src/api/app/models/obs_factory/openqa_job.rb @@ -0,0 +1,123 @@ +module ObsFactory + # Local representation of a job in the remote openQA. Uses a OpenqaApi (with a + # hardcoded base url) to read the information and the Rails cache to store it + class OpenqaJob + include ActiveModel::Model + extend ActiveModel::Naming + include ActiveModel::Serializers::JSON + + attr_accessor :id, :name, :state, :result, :clone_id, :iso, :modules, :settings + + def self.openqa_base_url + # build.opensuse.org can reach only the host directly, so we need + # to use http - and accept a https redirect if used on work stations + CONFIG['openqa_base_url'] || "http://openqa.opensuse.org" + end + + def self.openqa_links_url + CONFIG['openqa_links_url'] || "https://openqa.opensuse.org" + end + + @@api = ObsFactory::OpenqaApi.new(openqa_base_url) + + # Reads jobs from the openQA instance or the cache with an interface similar + # to ActiveRecord::Base#find_all_by + # + # If searching by iso or getting the full list, caching comes into play. In + # any other case, a GET query to openQA is always performed. + # + # param [Hash] args filters to use in the query. Valid values: + # :build, :distri, :iso, :maxage, :state, :group and :version + # param [Hash] opt Options: + # :cache == 'refresh' forces a refresh of the cache + # :exclude_modules skips the loading of the modules information (which + # needs an extra GET request per job). The #modules atribute will be + # empty for all the jobs (except those read from the cache) and the + # results will not be cached + def self.find_all_by(args = {}, opt = {}) + refresh = (opt.symbolize_keys[:cache].to_s == 'refresh') + exclude_mod = !!opt.symbolize_keys[:exclude_modules] + filter = args.symbolize_keys.slice(:iso, :state, :build, :maxage, :distri, :version, :group) + + # We are only interested in current results + get_params = {scope: 'current'} + + # If searching for the whole list of jobs, it caches the jobs + # per ISO name. + if filter.empty? + Rails.cache.delete('openqa_isos') if refresh + jobs = [] + isos = Rails.cache.read('openqa_isos') + # If isos are in the cache, everything is read from cache + if isos + (isos + [nil]).each do |iso| + jobs += Rails.cache.read("openqa_jobs_for_iso_#{iso}") || [] + end + else + # Get the bare list of jobs + jobs = @@api.get('jobs', get_params)['jobs'] + # If exclude_mod is given, that's all. But if not... + unless exclude_mod + # First, enrich the result with the modules information and cache + # the jobs per ISO + jobs.group_by {|j| (j['assets']['iso'].first rescue nil)}.each do |iso, iso_jobs| + Rails.cache.write("openqa_jobs_for_iso_#{iso}", iso_jobs, expires_in: 2.minutes) + end + # And then, cache the list of ISOs + isos = jobs.map {|j| (j['assets']['iso'].first rescue nil)}.sort.compact.uniq + Rails.cache.write('openqa_isos', isos, expires_in: 2.minutes) + end + end + # If searching only by ISO, cache that one + elsif filter.keys == [:iso] + cache_entry = "openqa_jobs_for_iso_#{filter[:iso]}" + Rails.cache.delete(cache_entry) if refresh + jobs = Rails.cache.read(cache_entry) + if jobs.nil? + get_params[:iso] = filter[:iso] + jobs = @@api.get('jobs', get_params)['jobs'] + unless exclude_mod + Rails.cache.write(cache_entry, jobs, expires_in: 2.minutes) + end + end + # In any other case, don't cache + else + get_params.merge!(filter) + jobs = @@api.get('jobs', get_params)['jobs'] + end + unless jobs.nil? + jobs.map { |j| OpenqaJob.new(j.slice(*attributes)) } + else + return Hash.new + end + end + + # Name of the modules which failed during openQA execution + # + # @return [Array] array of module names + def failing_modules + modules.reject {|m| %w(passed softfailed running none).include? m['result']}.map {|m| m['name'] } + end + + # Result of the job, or its state if no result is available yet + # + # @return [String] state if result is 'none', value of result otherwise + def result_or_state + if result == 'none' + state + else + result + end + end + + def self.attributes + %w(id name state result clone_id iso modules settings) + end + + # Required by ActiveModel::Serializers + def attributes + Hash[self.class.attributes.map { |a| [a, nil] }] + end + + end +end diff --git a/src/api/app/models/obs_factory/request.rb b/src/api/app/models/obs_factory/request.rb new file mode 100644 index 00000000000..1a6c4bfcf63 --- /dev/null +++ b/src/api/app/models/obs_factory/request.rb @@ -0,0 +1,124 @@ +module ObsFactory + # A covenient subset of BsRequest. + # + # It contains a reference to the corresponding BsRequest object, but exposing + # the attributes and methods that are relevant to the engine in a more + # convenient way. + class Request + include ActiveModel::Model + extend ActiveModel::Naming + include ActiveModel::Serializers::JSON + + attr_accessor :bs_request + + OBSOLETE_STATES = %w(declined superseded revoked) + + DELEGATED_METHODS = %w(id state description creator accept_at created_at updated_at reviews number) + + # Delegate some methods to the associated BsRequest object + DELEGATED_METHODS.each do |m| + define_method(m) { bs_request.send(m) } + end + + def initialize(bs_request = nil) + self.bs_request = bs_request + end + + # Requests with the given ids + # + # @param [Array] Array of ids + # @return [Array] Array of Request objects + def self.find(ids) + bs_requests = BsRequest.includes(:reviews, :bs_request_actions).where(number: ids) + bs_requests.map {|r| Request.new(r) } + end + + # Requests in 'review' state that have new reviews for the given project + # + # @param [Hash] props can contain :by_project, :by_group, :by_user, :by_package + # or :target_project + # @return [Array] Array of Request objects + def self.with_open_reviews_for(props) + reviews = Review.includes(:bs_request => [:reviews, :bs_request_actions]) + conds = props.dup + target_project = conds.delete(:target_project) + reviews = reviews.where(conds.merge(state: 'new', "bs_requests.state" => 'review')) + if target_project + reviews = reviews.where("bs_request_actions.target_project" => target_project) + end + reviews.map {|r| Request.new(r.bs_request) } + end + + def self.in_state_new(props) + reviews = Review.includes(:bs_request => [:reviews, :bs_request_actions]) + conds = props.dup + target_project = conds.delete(:target_project) + reviews = reviews.where(conds.merge("bs_requests.state" => 'new')) + if target_project + reviews = reviews.where("bs_request_actions.target_project" => target_project) + end + reviews.map {|r| Request.new(r.bs_request) } + end + + # Checks if the request is obsolete + # + # @return [Boolean] true if the request is declined, superseded or revoked + def obsolete? + OBSOLETE_STATES.include? state.to_s + end + + # Name of the original target package + # + # return [String] the name + def package + bs_request.bs_request_actions.first.target_package + end + + def request_type + bs_request.bs_request_actions.first.type + end + + # Name of the original target project + # + # return [String] the name + def project + bs_request.bs_request_actions.first.target_project + end + + # Id of the superseding request + # + # @return [Integer] id of request or nil if none + def superseded_by_id + bs_request.superseded_by ? (bs_request.superseded_by.to_i rescue nil) : nil + end + + # Superseding request + # + # @return [Request] the request or nil if none + def superseded_by + superseded_by_id ? Request.find(superseded_by_id) : nil + end + + def ==(req) + id == req.id + end + + def eql?(req) + id == req.id + end + + # Defined to enable the usage of Array#- + def hash + id.hash + 32 * bs_request.hash + end + + def self.attributes + DELEGATED_METHODS + %w(package superseded_by_id) - %w(reviews) + end + + # Required by ActiveModel::Serializers + def attributes + Hash[self.class.attributes.map { |a| [a, nil] }] + end + end +end diff --git a/src/api/app/models/obs_factory/staging_project.rb b/src/api/app/models/obs_factory/staging_project.rb new file mode 100644 index 00000000000..9a3448f9b78 --- /dev/null +++ b/src/api/app/models/obs_factory/staging_project.rb @@ -0,0 +1,365 @@ +module ObsFactory + # A staging project asociated to a distribution. + # + # It contains references to the corresponding Project and + # Distribution objects. + class StagingProject + include ActiveModel::Model + extend ActiveModel::Naming + include ActiveModel::Serializers::JSON + + attr_accessor :project, :distribution, :parent + + OBSOLETE_STATES = %w(declined superseded revoked) + NAME_PREFIX = ":Staging:" + ADI_NAME_PREFIX = ":Staging:adi:" + + def initialize(project, distribution) + self.project = project + self.distribution = distribution + end + + # Find all staging projects for a given distribution + # + # @param [Boolean] only_letter only letter stagings, otherwise all stagings + # @return [Array] array of StagingProject objects + def self.for(distribution, only_letter=true) + wildcard = only_letter ? "_" : "%" + ::Project.where(["name like ?", "#{distribution.root_project_name}#{NAME_PREFIX}#{wildcard}"]).map { |p| StagingProject.new(p, distribution) } + end + + # Find a staging project by distribution and id + # + # @return [StagingProject] the project + def self.find(distribution, id) + project = ::Project.find_by_name("#{distribution.root_project_name}#{NAME_PREFIX}#{id}") + if project + StagingProject.new(project, distribution) + else + nil + end + end + + # Name of the associated project + # + # @return [String] name of the Project object + def name + project.name + end + + # Description of the associated project + # + # @return [String] description of the Project object + def description + project.description + end + + # Checks if the project is adi staging project + # + # @return [Boolean] true if the project is adi staging project + def adi_staging? + if name =~ /#{ADI_NAME_PREFIX}/ + return true + end + false + end + + # Part of the name shared by all the staging projects belonging to the same + # distribution + # + # @return [String] the name excluding the id + def prefix + if adi_staging? + "#{distribution.root_project_name}#{ADI_NAME_PREFIX}" + else + "#{distribution.root_project_name}#{NAME_PREFIX}" + end + end + + # Letter of the staging project, extracted from its name + # + # @return [String] just the letter + def letter + adi_staging? ? name[prefix.size..-1] : name[prefix.size, 1] + end + + # Id of the staging project, extracted from its name + # + # @return [String] the name excluding the common prefix + def id + if adi_staging? + 'adi:' + name[prefix.size..-1] + else + name[prefix.size..-1] + end + end + + # Requests that are selected into the project but should be not longer valid + # due to its state (declined, superseded or revoked). + # + # @return [ActiveRecord::Relation] Obsolete requests + def obsolete_requests + selected_requests.select(&:obsolete?) + end + + # Used to fetch the :DVD project + # + # @return StagingProject object or nil + def subproject + return @subprojects[0] unless @subprojects.nil? + @subprojects = [] + ::Project.where(["name like ?", "#{name}:%"]).map do |p| + p = StagingProject.new(p, distribution) + p.parent = self + @subprojects << p + end + if @subprojects.length > 1 + Rails.logger.error 'There too many subprojects, we can not handle this' + @subprojects[0] = nil + return + end + @subprojects[0] ||= nil + end + + # only for compat in the JSON + def subprojects + [ subproject ] + end + + # Associated openQA jobs. + # + # The jobs are fetched by ISO name. + # @see #iso + # + # @return [Array] Array of OpenqaJob objects + def openqa_jobs + return @openqa_jobs unless @openqa_jobs.nil? + @openqa_jobs ||= openqa_results_relevant? ? OpenqaJob.find_all_by(iso: iso) : [] + end + + # only check the openqa jobs if the project is under specific conditions + def openqa_results_relevant? + return false if iso.nil? + return false if overall_state == :building + if parent + return parent.openqa_results_relevant? + else # master project + return ! [:building, :empty].include?(overall_state) + end + end + + # Packages included in the staging project that are not building properly. + # + # Every broken package is represented by a hash with the following keys: + # 'package', 'state', 'details', 'repository', 'arch' + # + # @return [Array] Array of hashes + def broken_packages + set_buildinfo if @broken_packages.nil? + @broken_packages + end + + # Repositories referenced in the staging project that are still building + # + # Every building repository is represented by a hash with the following keys: + # 'repository', 'arch', 'code', 'state', 'dirty' + # + # @return [Array] Array of hashes + def building_repositories + set_buildinfo if @building_repositories.nil? + @building_repositories + end + + # Requests with open reviews but that are not selected into the staging + # project + # + # @return [Array] Array of Request objects + def untracked_requests + open_requests - selected_requests + end + + # Requests with open reviews + # + # @return [Array] Array of BsRequest objects + def open_requests + @open_requests ||= Request.with_open_reviews_for(by_project: name) + end + + # Requests selected in the project + # + # @return [Array] Array of BsRequest objects + def selected_requests + if @selected_requests.nil? + requests = meta["requests"] + if requests + ids = requests.map { |i| i['id'].to_i } + @selected_requests = Request.find(ids) + else + @selected_requests = [] + end + end + @selected_requests + end + + # Reviews that need to be accepted in order to be able to accept the + # project. + # + # Reviews associated with the project that either are not accepted either + # have the 'by_project' attribute set to the staging project. + # + # @return [Array] array of hashes with the following keys: :id, :state, + # :request, :package and :by. + def missing_reviews + if @missing_reviews.nil? + @missing_reviews = [] + attribs = [:by_group, :by_user, :by_project, :by_package] + + (open_requests + selected_requests).uniq.each do |req| + req.reviews.each do |rev| + unless rev.state.to_s == 'accepted' || rev.by_project == name + # FIXME: this loop (and the inner if) would not be needed + # if every review only has one valid by_xxx. + # I'm keeping it to mimic the python implementation. + # Instead, we could have something like + # who = rev.by_group || rev.by_user || rev.by_project || rev.by_package + attribs.each do |att| + if who = rev.send(att) + @missing_reviews << { id: rev.id, request: req.number, state: rev.state.to_s, package: req.package, by: who } + end + end + end + end + end + end + @missing_reviews + end + + # Metadata stored in the description field + # + # @return [Hash] Hash with the metadata (currently the list of requests) + def meta + @meta ||= YAML.load(description) || {} + end + + # Name of the ISO file generated by the staging project. + # + # @return [String] ISO file name + def iso + return @iso if @iso + @iso = distribution.openqa_iso(self) + end + + def self.attributes + %w(name description obsolete_requests openqa_jobs building_repositories + broken_packages subproject subprojects untracked_requests missing_reviews selected_requests overall_state ) + end + + # Required by ActiveModel::Serializers + def attributes + Hash[self.class.attributes.map { |a| [a, nil] }] + end + + def build_state + return :building if building_repositories.present? + return :failed if broken_packages.present? + :acceptable + end + + # check openQA jobs for all projects not building right now - or that are known to be broken + def openqa_state + # no openqa result for adi staging project + return :acceptable if adi_staging? + # the ISOs may still be syncing + return :testing if openqa_jobs.empty? + + openqa_jobs.each do |job| + if job.failing_modules.present? + return :failed + elsif ! %w(passed softfailed).include? job.result + return :testing + end + end + :acceptable + end + + # calculate the overall state of the project + def overall_state + return @state unless @state.nil? + @state = :empty + + if selected_requests.empty? + return @state + end + + # base state + if untracked_requests.present? || obsolete_requests.present? + @state = :unacceptable + else + @state = build_state + end + + if @state == :acceptable && subproject + @state = subproject.build_state + end + + if @state == :acceptable + @state = openqa_state + if @state == :acceptable && subproject + @state = subproject.openqa_state + end + end + + if @state == :acceptable && missing_reviews.present? + @state = :review + end + + @state + end + + protected + + # Used internally to calculate #broken_packages and #building_repositories + def set_buildinfo + buildresult = Buildresult.find_hashed(project: name, code: %w(failed broken unresolvable)) + @broken_packages = [] + @building_repositories = [] + buildresult.elements('result') do |result| + building = false + if !%w(published unpublished).include?(result['state']) || result['dirty'] == 'true' + building = true + end + result.elements('status') do |status| + code = status.get('code') + if %w(broken failed).include?(code) || (code == 'unresolvable' && !building) + @broken_packages << { 'package' => status['package'], + 'project' => name, + 'state' => code, + 'details' => status['details'], + 'repository' => result['repository'], + 'arch' => result['arch'] } + end + end + if building + # determine build summary + current_repo = result.slice('repository', 'arch', 'code', 'state', 'dirty') + current_repo[:tobuild] = 0 + current_repo[:final] = 0 + + buildresult = Buildresult.find_hashed(project: name, view: 'summary', repository: current_repo['repository'], arch: current_repo['arch']) + buildresult = buildresult.get('result').get('summary') + buildresult.elements('statuscount') do |sc| + if %w(excluded broken failed unresolvable succeeded excluded disabled).include?(sc['code']) + current_repo[:final] += sc['count'].to_i + else + current_repo[:tobuild] += sc['count'].to_i + end + end + @building_repositories << current_repo + end + end + if @building_repositories.present? + @broken_packages = @broken_packages.select { |p| p['state'] != 'unresolvable' } + end + end + end +end diff --git a/src/api/app/presenters/obs_factory/base_presenter.rb b/src/api/app/presenters/obs_factory/base_presenter.rb new file mode 100644 index 00000000000..799d6146ec1 --- /dev/null +++ b/src/api/app/presenters/obs_factory/base_presenter.rb @@ -0,0 +1,25 @@ +module ObsFactory + # Extremely simple implementation of the Decorator pattern for the views. + # + # At some point, it would make sense to adopt Draper or any + # other gem implementing more full-featured decorators. + # https://github.com/drapergem/draper + class BasePresenter < SimpleDelegator + # Decorate a collection of objects + # + # @params [#map] collection objects to decorate + # @return [Array] array of presenter objects + def self.wrap(collection) + collection.map do |obj| + new obj + end + end + + # The original object + # + # @return [Object] object without decoration + def model + __getobj__ + end + end +end diff --git a/src/api/app/presenters/obs_factory/obs_project_presenter.rb b/src/api/app/presenters/obs_factory/obs_project_presenter.rb new file mode 100644 index 00000000000..4e7b53e89c0 --- /dev/null +++ b/src/api/app/presenters/obs_factory/obs_project_presenter.rb @@ -0,0 +1,81 @@ +module ObsFactory + + # View decorator for a Project + class ObsProjectPresenter < BasePresenter + + + def build_and_failed_params + params = { project: self.name, defaults: 0 } + Buildresult.avail_status_values.each do |s| + next if %w(succeeded excluded disabled).include? s.to_s + params[s] = 1 + end + + self.repos.each do |r| + next if exclusive_repository && r != exclusive_repository + params["repo_#{r}"] = 1 + end + # hard code the archs we care for + params['arch_i586'] = 1 + params['arch_x86_64'] = 1 + params['arch_ppc64le'] = 1 + params['arch_local'] = 1 + params + end + + + def summary + return @summary if @summary + building = false + failed = 0 + final = 0 + total = 0 + + # first calculate the building state - and filter the results + results = [] + build_summary.elements('result') do |result| + next if exclusive_repository && result['repository'] != exclusive_repository + if !%w(published unpublished unknown).include?(result['state']) || result['dirty'] == 'true' + building = true + end + results << result + end + results.each do |result| + result['summary'].elements('statuscount') do |sc| + code = sc['code'] + count = sc['count'].to_i + next if code == 'excluded' # plain ignore + total += count + if code == 'unresolvable' + unless building # only count if finished + failed += count + end + next + end + if %w(broken failed).include?(code) + failed += count + elsif %w(succeeded disabled).include?(code) + final += count + end + end + end + if failed > 0 + failed = build_failures_count + end + if building + build_progress = (100 * (final + failed)) / total + build_progress = [99, build_progress].min + if failed > 0 + @summary = [:building, "#{self.nickname}: #{build_progress}% (#{failed} errors)"] + else + @summary = [:building, "#{self.nickname}: #{build_progress}%"] + end + elsif failed > 0 + # don't duplicate packages in archs, so redo + @summary = [:failed, "#{self.nickname}: #{failed} errors"] + else + @summary = [:succeeded, "#{self.nickname}: DONE"] + end + end + end +end diff --git a/src/api/app/presenters/obs_factory/openqa_job_presenter.rb b/src/api/app/presenters/obs_factory/openqa_job_presenter.rb new file mode 100644 index 00000000000..fb30bf4e23d --- /dev/null +++ b/src/api/app/presenters/obs_factory/openqa_job_presenter.rb @@ -0,0 +1,18 @@ +module ObsFactory + # View decorator for OpenqaJob + class OpenqaJobPresenter < BasePresenter + # URL of the job in the openQA instance + # + # @return [String] the full URL + def url + OpenqaJob.openqa_links_url.chomp('/') + "/tests/#{id}" + end + + # The part of the name that refers to the testsuite + # + # @return [String] type of test + def test + settings['TEST'] + end + end +end diff --git a/src/api/app/presenters/obs_factory/staging_project_presenter.rb b/src/api/app/presenters/obs_factory/staging_project_presenter.rb new file mode 100644 index 00000000000..c874af3c5c5 --- /dev/null +++ b/src/api/app/presenters/obs_factory/staging_project_presenter.rb @@ -0,0 +1,192 @@ +module ObsFactory + # View decorator for StagingProject + class StagingProjectPresenter < BasePresenter + + # Wraps the associated subproject with the corresponding decorator. + # + # @return StagingProjectPresenter object + def subproject + return nil unless model.subproject + ObsFactory::StagingProjectPresenter.new(model.subproject) + end + + def self.sort(collection) + prjs = wrap(collection) + prjs.sort_by! { |a| a.sort_key } + end + + # List of packages included in the staging_project. + # + # The names are extracted from the description (that is in fact a yaml + # string). + # + # @return [String] package names delimited by commas + def description_packages + requests = meta["requests"] + if requests.blank? + '' + else + requests.map { |i| i["package"] }.sort.join(', ') + end + end + + # engine helpers are troublesome, so we avoid them + def self.review_icon(reviewer) + case reviewer + when 'opensuse-review-team' then + 'search' + when 'repo-checker' then + 'cog' + when 'sle-release-managers', 'leap-reviewers', 'caasp-release-managers' then + 'users' + when 'maintenance-team' then + 'medkit' + when 'legal-team', 'legal-auto' then + 'graduation-cap' + when 'leaper' then + 'code-fork' + when 'sle-changelog-checker' then + 'tags' + else + 'ban' + end + end + + # List of requests/packages tracked in the staging project + def classified_requests + return @classified_requests if @classified_requests + + @classified_requests = [] + requests = selected_requests + return @classified_requests unless requests + + reviews = Hash.new + missing_reviews.each do |r| + reviews[r[:request]] ||= [] + r[:icon] = self.class.review_icon(r[:by]) + reviews[r[:request]] << r + end + requests.each do |req| + r = { number: req.number, package: req.package } + css = 'ok' + r[:missing_reviews] = reviews[req.number] + unless r[:missing_reviews].blank? + css = 'review' + end + if req.obsolete? + css = 'obsolete' + end + r[:css] = css + r[:request_type] = req.request_type + @classified_requests << r + end + # now append untracked reqs + untracked_requests.each do |req| + @classified_requests << { number: req.number, package: req.package, css: 'untracked' } + end + @classified_requests.sort! { |x, y| x[:package] <=> y[:package] } + @classified_requests + end + + # determine build progress as percentage + # if the project contains subprojects but is complete, the percentage + # is the subproject's + def build_progress + total = 0 + final = 0 + building_repositories.each do |r| + total += r[:tobuild] + r[:final] + final += r[:final] + end + ret = { subproject: name } + if total != 0 + # if we have building repositories, make sure we don't exceed 99 + ret[:percentage] = [final * 100 / total, 99].min + else + ret[:percentage] = 100 + return subproject.build_progress if subproject + end + ret + end + + # collect the broken packages of all subprojects + def broken_packages + ret = model.broken_packages + ret += subproject.broken_packages if subproject + ret + end + + # @return [Array] Array of OpenqaJob objects for all subprojects + def all_openqa_jobs + ret = model.openqa_jobs + ret += subproject.openqa_jobs if subproject + ret + end + + # Wraps the associated openqa_jobs with the corresponding decorator. + # + # @return [Array] Array of OpenqaJobPresenter objects for all subprojects + def openqa_jobs + ObsFactory::OpenqaJobPresenter.wrap(all_openqa_jobs) + end + + # Wraps the failed openqa_jobs with the corresponding decorator. + # + # @return [Array] Array of OpenqaJobPresenter objects for all subprojects + def failed_openqa_jobs + ObsFactory::OpenqaJobPresenter.wrap(all_openqa_jobs.select { |job| job.failing_modules.present? }) + end + + # return a percentage counting the reviewed requests / total requests + def review_percentage + total = classified_requests.size + missing = 0 + classified_requests.each do |rq| + missing += 1 if rq[:missing_reviews] + end + if total > 0 + 100 - missing * 100 / total + else + 0 + end + end + + def testing_percentage + jobs = all_openqa_jobs + notdone = 0 + jobs.each do |job| + notdone += 1 unless %w(passed failed).include?(job.result) + end + if jobs.size > 0 + 100 - notdone * 100 / jobs.size + else + 0 + end + end + + # returns a number presenting how high it should be in the list of staging prjs + # the lower the number, the earlier it is in the list - acceptable A first + def sort_key + main = case overall_state + when :acceptable then + 0 + when :review then + 10000 - review_percentage * 100 + when :testing then + 20000 - testing_percentage * 100 + when :building then + 30000 - build_progress[:percentage] * 100 + when :failed then + 40000 + when :unacceptable then + 50000 + when :empty + 60000 + else + Rails.logger.error "untracked #{overall_state}" + return + end + main + letter.ord() + end + end +end diff --git a/src/api/app/views/layouts/webui/obs_factory/application.html.erb b/src/api/app/views/layouts/webui/obs_factory/application.html.erb new file mode 100644 index 00000000000..80e39ebf60b --- /dev/null +++ b/src/api/app/views/layouts/webui/obs_factory/application.html.erb @@ -0,0 +1,8 @@ +<% @pagetitle = "#{@distribution.name} Staging Dashboard" + @layouttype = 'custom' +%> +<% content_for :content_for_head do -%> + <%= stylesheet_link_tag 'webui/obs_factory/application', media: 'all' %> + +<% end -%> +<%= render :template => "layouts/webui/webui" %> diff --git a/src/api/app/views/webui/obs_factory/distributions/show.html.haml b/src/api/app/views/webui/obs_factory/distributions/show.html.haml new file mode 100644 index 00000000000..57086b87a80 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/distributions/show.html.haml @@ -0,0 +1,98 @@ +- project_bread_crumb 'Dashboard' + +.grid_16.alpha + .box.box-shadow + %h2 + %i.fa.fa-2.fa-tasks + = link_to 'Staging Projects', staging_projects_path(project: @distribution.name) + %ul#main-dashboard + - @staging_projects.each do |project| + - next if project.overall_state == :empty + %li + = render partial: 'webui/obs_factory/staging_projects/overall_state', locals: { project: project } + + %p.clear + + %h2 + %i.fa.fa-2.fa-circle-o + = link_to 'Ring Projects', main_app.project_subprojects_path(project: @distribution.rings_project_name) + %ul.project-results#ring-projects + - @ring_prjs.each do |rp| + %li{class: "project-result-#{rp.summary[0]}"} + = link_to rp.summary[1], main_app.project_monitor_path(rp.build_and_failed_params) + + %p.clear + + %h2 + %i.fa.fa-2.fa-building + = link_to "Repositories of #{@distribution.name}", main_app.project_show_path(project: "#{@distribution.name}") + + %ul.project-results#factory-build-status + %li{class: "project-result-#{@standard.summary[0]} "} + = link_to @standard.summary[1], main_app.project_monitor_path(@standard.build_and_failed_params) + - if @live + %li{class: "project-result-#{@live.summary[0]}"} + = link_to @live.summary[1], main_app.project_monitor_path(@live.build_and_failed_params) + %li{class: "project-result-#{@images.summary[0]}"} + = link_to @images.summary[1], main_app.project_monitor_path(@images.build_and_failed_params) + + + %p.clear + + %h2 + %i.fa.fa-2.fa-check + = link_to 'Open Reviews', "/project/requests/#{@distribution.root_project_name}" + + %ul + - if @reviews[:legal_auto] > 0 + %li + Legal Auto: + = @reviews[:legal_auto] + + - if @reviews[:factory_auto] > 0 + %li + Factory Auto: + = @reviews[:factory_auto] + + %li + openSUSE Review Team: + = @reviews[:review_team] + + %li + Repo Checker: + = @reviews[:repo_checker] + + %li + Legal Team: + = @reviews[:legal_team] + %h2 + %i.fa.fa-2.fa-tag + = link_to 'Versions', "http://download.opensuse.org/#{@distribution.url_suffix}" + + %ul + - if @versions[:source] + %li + Source: + = link_to @versions[:source], main_app.package_show_path(project: "#{@distribution.name}", package: '000product') + + - if @versions[:totest] + %li + Testing: + = link_to @versions[:totest], "#{openqa_links_helper}/tests/overview?distri=opensuse&version=#{@distribution.openqa_version}&group=#{@distribution.openqa_group}" + + - if @versions[:published] + %li + Published: + = link_to @versions[:published], "http://download.opensuse.org/#{@distribution.url_suffix}" + + %h2 + %i.fa.fa-2.fa-wrench + = link_to "openQA results for #{@versions[:totest]}", "#{openqa_links_helper}/tests/overview?distri=opensuse&version=#{@distribution.openqa_version}&build=#{@versions[:totest]}&group=#{@distribution.openqa_group}" + + %p + - unless @openqa_jobs.empty? + - @openqa_jobs.group_by(&:result_or_state).each do |label, jobs| + %b #{label}: + = jobs.size + - else + OpenQA currently not reachable or failure in GET request diff --git a/src/api/app/views/webui/obs_factory/openqa_jobs/_openqa_job.html.erb b/src/api/app/views/webui/obs_factory/openqa_jobs/_openqa_job.html.erb new file mode 100644 index 00000000000..e6e3969a4a1 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/openqa_jobs/_openqa_job.html.erb @@ -0,0 +1,10 @@ +<% + if openqa_job.failing_modules.empty? + css = 'openqa-'+openqa_job.result + label = openqa_job.test + else + css = 'openqa-failed' + label = openqa_job.failing_modules.first + end +%> +<%= link_to label, openqa_job.url, class: css %> diff --git a/src/api/app/views/webui/obs_factory/requests/_request.html.erb b/src/api/app/views/webui/obs_factory/requests/_request.html.erb new file mode 100644 index 00000000000..1dceca7cbdf --- /dev/null +++ b/src/api/app/views/webui/obs_factory/requests/_request.html.erb @@ -0,0 +1 @@ +<%= link_to "##{request.number} (#{request.state})", main_app.request_show_url(request.bs_request) %> diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_buildinfo.html.erb b/src/api/app/views/webui/obs_factory/staging_projects/_buildinfo.html.erb new file mode 100644 index 00000000000..8de2c37ff0f --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_buildinfo.html.erb @@ -0,0 +1,27 @@ +<% ok = project.building_repositories.empty? -%> +
<%= sprite_tag(ok ? 'accept' : 'error', alt: ok ? 'Ok' : 'Fail') %>Building repositories
+
+<% if ok %> + None. +<% else %> + <% building = project.building_repositories.map do |repo| %> + <% link_to "#{repo['repository']}-#{repo['arch']} (#{repo['state']})".html_safe, + main_app.project_repository_state_url(project.name, repo['repository']) %> + <% end %> + <%= building.to_sentence.html_safe %> +<% end %> +
+<% ok = project.broken_packages.size.zero? -%> +
<%= sprite_tag(ok ? 'accept' : 'error', alt: ok ? 'Ok' : 'Fail') %>Broken packages
+
+<% if ok %> + None. +<% else %> + <% broken = project.broken_packages.map do |pack| %> + <% link_to "#{pack['package']} (#{pack['state']})".html_safe, + main_app.package_show_url(project.name, pack['package']) %> + <% end %> + <%= broken.to_sentence.html_safe %> +<% end %> +
+ diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_expand_collapse.js.erb b/src/api/app/views/webui/obs_factory/staging_projects/_expand_collapse.js.erb new file mode 100644 index 00000000000..08a84ff617a --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_expand_collapse.js.erb @@ -0,0 +1,12 @@ +$('a.staging_expand').click(function() { + var css = 'staging_'+$(this).data('name'); + $('.staging_expand.'+css).hide('appear'); + $('.staging_collapsible.'+css).show('slide'); + return false; +}); +$('a.staging_collapse').click(function() { + var css = 'staging_'+$(this).data('name'); + $('.staging_collapsible.'+css).hide('slide'); + $('.staging_expand.'+css).show('appear'); + return false; +}); diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_infos.html.haml b/src/api/app/views/webui/obs_factory/staging_projects/_infos.html.haml new file mode 100644 index 00000000000..cf4a823f912 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_infos.html.haml @@ -0,0 +1,48 @@ +.box.box-shadow + %h2.box-header Infos + + %p Empty Projects: + - empties = @staging_projects.select { |p| p.overall_state == :empty && p.name =~ /Staging:[A-Z]?$/ } + - empties = empties.map { |p| link_to(p.letter, main_app.project_show_path(p.name)) } + + %p + - if empties.empty? + none + - else + = empties.sort.join(' – ').html_safe + + %p= link_to 'Backlog:', main_app.group_show_path(title: @distribution.staging_manager) + - if @backlog_requests.empty? + %p + %i Empty + - else + %ul.staging_backlog + - @backlog_requests.each_with_index do |req,counter| + %li + = link_to elide(req.package, length = 19), main_app.request_show_path(req.number) + - if counter > 5 && @backlog_requests.size > counter + 1 + %li ... #{@backlog_requests.size - counter} more + - break + + %p= link_to 'Ready:', "/project/requests/#{@distribution.project}?state=new" + - if @requests_state_new.empty? + %p + %i Empty + - else + %ul.staging_backlog + - @requests_state_new.each_with_index do |req,counter| + %li + = link_to elide(req.package, length = 19), main_app.request_show_path(req.number) + - if counter > 5 && @requests_state_new.size > counter + 1 + %li ... #{@requests_state_new.size - counter} more + - break + + - if @backlog_requests_ignored.present? + %p= link_to 'Ignored:', "/package/view_file/#{@distribution.project}:Staging/dashboard/ignored_requests?expand=1" + %ul.staging_backlog_ignored + - @backlog_requests_ignored.each_with_index do |req,counter| + %li + = link_to elide(req.package, length = 19), main_app.request_show_path(req.number), title: @ignored_requests[req.number] + - if counter > 5 && @backlog_requests_ignored.size > counter + 1 + %li ... #{@backlog_requests_ignored.size - counter} more + - break diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_legend.html.haml b/src/api/app/views/webui/obs_factory/staging_projects/_legend.html.haml new file mode 100644 index 00000000000..92645c20b56 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_legend.html.haml @@ -0,0 +1,53 @@ +.box.box-shadow#legends + %h2.box-header Legend + + %p Requests + %ul.color-legend + %li + %span.ok + Ready + %li + %span.review + In review + %li + %span.obsolete + Obsolete + %li + %span.untracked + Untracked + %li.delete + %span.delete + Delete + + %p Projects + %ul.color-legend + %li + %span.staging-project.state-building + Building + %li + %span.staging-project.state-testing + Testing + %li + %span.staging-project.state-review + Review + %li + %span.staging-project.state-acceptable + Acceptable + %li + %span.staging-project.state-unacceptable + Unacceptable + %li + %span.staging-project.state-failed + Failed + + %p Reviews + %ul.color-legend + %li + %i{class: "fa fa-#{ObsFactory::StagingProjectPresenter.review_icon('opensuse-review-team')}"} + = link_to 'Source Review', main_app.group_show_path('opensuse-review-team') + %li + %i{class: "fa fa-#{ObsFactory::StagingProjectPresenter.review_icon('repo-checker')}"} + = link_to 'Installation Check', main_app.user_show_path('repo-checker') + %li + %i{class: "fa fa-#{ObsFactory::StagingProjectPresenter.review_icon('legal-team')}"} + = link_to 'Legal Review', main_app.group_show_path('legal-team') diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_openqa_jobs.html.erb b/src/api/app/views/webui/obs_factory/staging_projects/_openqa_jobs.html.erb new file mode 100644 index 00000000000..ab833b7ebbf --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_openqa_jobs.html.erb @@ -0,0 +1,13 @@ +<% ok = project.openqa_jobs.all? {|j| j.result == 'passed' } -%> +
<%= sprite_tag(ok ? 'accept' : 'error', alt: ok ? 'Ok' : 'Fail') %>openQA Jobs
+
+<% if project.openqa_jobs.empty? %> + None. +<% else %> + +<% end %> +
diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_overall_state.html.haml b/src/api/app/views/webui/obs_factory/staging_projects/_overall_state.html.haml new file mode 100644 index 00000000000..f664f33c6e5 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_overall_state.html.haml @@ -0,0 +1,22 @@ +%div{class: "overall-state staging-project state-#{project.overall_state}"} + .letter + = link_to project.letter, staging_project_path(project: @distribution.name, project_name: project.id) + .state + = link_to project.overall_state, main_app.project_show_path(project.name) + .progress + - if project.overall_state == :building + - progress = project.build_progress + = link_to main_app.project_monitor_url(progress[:subproject]) do + #{progress[:percentage]}% + - elsif project.overall_state == :review + #{project.review_percentage}% + - elsif project.overall_state == :testing + = link_to "#{openqa_links_helper}/tests?hoursfresh=24&#{@distribution.openqa_filter(project)}" do + #{project.testing_percentage}% + - if project.meta['splitter_info'] + .splitter-info{ title: 'strategy information (name, group)'} + - splitter_info = project.meta['splitter_info'] + .splitter-info-strategy + #{splitter_info['strategy']['name']} + .splitter-info-group + #{splitter_info['group']} diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_packages_list.html.haml b/src/api/app/views/webui/obs_factory/staging_projects/_packages_list.html.haml new file mode 100644 index 00000000000..92a3b81ee34 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_packages_list.html.haml @@ -0,0 +1,12 @@ +%ul.packages-list + - max = 15 + - count = project.classified_requests.count + - if count <= max + 1 # Prevent the "1 more" link + = render partial: "request", collection: project.classified_requests + - else + = render partial: "request", collection: project.classified_requests[0,max] + - name = project.letter + %li.request= link_to "...#{count - max} more", "#", class: "staging_expand staging_#{name}", data: {name: name} + %div{class: "staging_collapsible staging_#{name}", style: "display:none"} + = render partial: "request", collection: project.classified_requests[max..-1] + %li.request= link_to "collapse", "#", class: "staging_collapse", data: {name: name} diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_problems.html.erb b/src/api/app/views/webui/obs_factory/staging_projects/_problems.html.erb new file mode 100644 index 00000000000..7caec54ad39 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_problems.html.erb @@ -0,0 +1,68 @@ + <% + problems = project.failed_openqa_jobs.map do |j| + content_tag(:li, "QA: #{render(j)}".html_safe, class: "openqa_job") + end +-%> + +<%# Failed packages are grouped by name %> +<% failed_packages = {} %> +<% other_broken_packages = [] %> +<% project.broken_packages.each do |pack| + name = pack['package'] + if pack['state'] == 'failed' + version = {arch: pack['arch'], project: pack['project']} + if failed_packages[name] + failed_packages[name][:versions] << version + else + failed_packages[name] = {repository: pack['repository'], versions: [version]} + end + else + other_broken_packages << {name: pack['package'], state: pack['state'], project: pack['project'] } + end +end -%> + +<% problems += failed_packages.map do |name, attrs| + versions = attrs[:versions].map do |v| + link_to(v[:arch], main_app.package_live_build_log_url(v.merge(package: name, repository: attrs[:repository]))) + end + label = "Build failed #{name} (" + versions.join(", ") + ")" + content_tag(:li, label.html_safe, class: "failed_pkg status_failed") +end -%> + +<% problems += other_broken_packages.map do |pack| + st = pack[:state] + name = pack[:name] + label = "Build #{st}: #{name}" + content_tag(:li, link_to(label.html_safe, main_app.package_show_url(pack[:project], name)), class: "failed_pkg status_#{st}") +end -%> + +<% if problems.empty? %> + +<% else %> + +<% end %> diff --git a/src/api/app/views/webui/obs_factory/staging_projects/_request.html.haml b/src/api/app/views/webui/obs_factory/staging_projects/_request.html.haml new file mode 100644 index 00000000000..ddd753e8091 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/_request.html.haml @@ -0,0 +1,5 @@ +%li{class: "#{request[:css]} #{request[:request_type]} request"} + = link_to request[:package], main_app.request_show_path(request[:number]) + - mrs = request[:missing_reviews] || [] + - mrs.each do |mr| + = link_to content_tag(:i, nil, class: "fa-#{mr[:icon]} fa missing_review"), main_app.request_show_path(request[:number]) diff --git a/src/api/app/views/webui/obs_factory/staging_projects/index.html.haml b/src/api/app/views/webui/obs_factory/staging_projects/index.html.haml new file mode 100644 index 00000000000..ce3cb7f7ece --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/index.html.haml @@ -0,0 +1,57 @@ +- project_bread_crumb 'Staging projects' + +- content_for :ready_function do + = render partial: 'expand_collapse.js.erb' + +.grid_13.alpha + .nav + %input#letterbox{checked: 'checked', name: 'navbar', type: 'radio'} + %section + %h2 + %label{for: 'letterbox'} Letter + .box.box-shadow + %table.dashboard.staging-dashboard + %thead + %tr + %th Project + %th Requests + %th Problems + + %tbody + - @staging_projects.each do |project| + - next if project.overall_state == :empty || project.name !~ /Staging:[A-Z]?$/ + %tr + %td + = render partial: 'overall_state', locals: { project: project } + %td + = render partial: 'packages_list', locals: { project: project } + %td.broken-packages + = render partial: 'problems', locals: { project: project } + + %input#adibox{name: 'navbar', type: 'radio'} + %section + %h2 + %label{for: 'adibox'} ADI + .box.box-shadow + %table.dashboard.staging-dashboard + %thead + %tr + %th Project + %th Requests + %th Problems + + %tbody + - @staging_projects.each do |project| + - next if project.overall_state == :empty || project.name !~ /Staging:adi:[0-9]+$/ + %tr + %td + = render partial: 'overall_state', locals: { project: project } + %td + = render partial: 'packages_list', locals: { project: project } + %td.broken-packages + = render partial: 'problems', locals: { project: project } + +.grid_3.omega + = render 'legend' + = render 'infos' + diff --git a/src/api/app/views/webui/obs_factory/staging_projects/show.html.erb b/src/api/app/views/webui/obs_factory/staging_projects/show.html.erb new file mode 100644 index 00000000000..56f027102d7 --- /dev/null +++ b/src/api/app/views/webui/obs_factory/staging_projects/show.html.erb @@ -0,0 +1,64 @@ +<% project_bread_crumb link_to('Staging projects', staging_projects_path(project: @distribution.name)), @staging_project.id %> + +
+ +
+

<%= @staging_project.name %>

+ +

Packages

+ +
+ <%= render partial: 'packages_list', locals: { project: @staging_project } %> +
+
+ +

Status

+
+
+ <% ok = @staging_project.untracked_requests.size.zero? -%> +
<%= sprite_tag(ok ? 'accept' : 'error', alt: ok ? 'Ok' : 'Fail') %>Untracked requests
+
+ <% if ok %> + None. + <% else %> + <%= render @staging_project.untracked_requests %> + <% end %> +
+ <% ok = @staging_project.obsolete_requests.size.zero? -%> +
<%= sprite_tag(ok ? 'accept' : 'error', alt: ok ? 'Ok' : 'Fail') %>Obsolete requests
+
+ <% if ok %> + None. + <% else %> + <%= render @staging_project.obsolete_requests %> + <% end %> +
+ <% ok = @staging_project.missing_reviews.size.zero? -%> +
<%= sprite_tag(ok ? 'accept' : 'error', alt: ok ? 'Ok' : 'Fail') %>Missing reviews
+
+ <% if ok %> + None. + <% else %> + <% missing = @staging_project.missing_reviews.map do |rev| %> + <% link_to "#{rev[:package]} by #{rev[:by]}".html_safe, main_app.request_show_url(number: rev[:request]) %> + <% end %> + <%= missing.to_sentence.html_safe %>. + <% end %> +
+ <%= render partial: 'buildinfo', locals: {project: @staging_project} %> + <%= render partial: 'openqa_jobs', locals: {project: @staging_project} %> +
+ <% if @staging_project.subproject %> +

DVD subproject

+
+ <%= render partial: 'buildinfo', locals: {project: @staging_project.subproject} %> + <%= render partial: 'openqa_jobs', locals: {project: @staging_project.subproject} %> +
+ <% end %> +
+
+
+ +
+ <%= render partial: 'legend'%> +
diff --git a/src/api/config/initializers/assets.rb b/src/api/config/initializers/assets.rb index 293886aefbb..7041e9d1e9c 100644 --- a/src/api/config/initializers/assets.rb +++ b/src/api/config/initializers/assets.rb @@ -8,4 +8,4 @@ # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += ['webui2/webui2.css', 'webui2/application.js'] +Rails.application.config.assets.precompile += ['webui2/webui2.css', 'webui2/application.js', 'webui/obs_factory/application.css'] diff --git a/src/api/config/initializers/inflector.rb b/src/api/config/initializers/inflector.rb new file mode 100644 index 00000000000..0258e9ebf9a --- /dev/null +++ b/src/api/config/initializers/inflector.rb @@ -0,0 +1,4 @@ +ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym "openSUSE" + inflect.acronym "OpenSUSE" +end diff --git a/src/api/config/routes.rb b/src/api/config/routes.rb index 0d4fcdabe9e..1cf359fdfcc 100644 --- a/src/api/config/routes.rb +++ b/src/api/config/routes.rb @@ -300,6 +300,10 @@ def self.public_or_about_path?(request) post 'project/unlock' => :unlock end + get 'project/dashboard/:project' => 'webui/obs_factory/distributions#show', as: 'dashboard', constraints: cons + get 'project/staging_projects/:project' => 'webui/obs_factory/staging_projects#index', as: 'staging_projects', constraints: cons + get 'project/staging_projects/:project/:project_name' => 'webui/obs_factory/staging_projects#show', as: 'staging_project', constraints: cons + controller 'webui/projects/rebuild_times' do get 'project/rebuild_time/:project/:repository/:arch' => :show, constraints: cons, as: 'project_rebuild_time' get 'project/rebuild_time_png/:project/:key' => :rebuild_time_png, constraints: cons