# Demo data for visual theme testing. # Run: docker compose exec redmine bin/rails runner /seed/seed.rb # Idempotent: exits if the demo project already exists. Dates are relative to today # so Gantt, calendar and roadmap always show current data. if Project.exists?(identifier: 'apollo') puts 'Demo data already present.' load File.join(__dir__, 'seed_extras.rb') exit end srand(42) today = Date.today # One transaction: a failure leaves no partial data. ActiveRecord::Base.transaction do Redmine::DefaultData::Loader.load('en') if Redmine::DefaultData::Loader.no_data? Setting.ui_theme = 'tonalmine' Setting.text_formatting = 'common_mark' Setting.notified_events = [] # demo data must not send mail admin = User.find_by(login: 'admin') admin.update!(must_change_passwd: false) User.current = admin people = [ %w[jdoe Jane Doe], %w[msmith Max Smith], %w[lnguyen Linh Nguyen], %w[pweber Paula Weber] ].map do |login, first, last| User.create!(login: login, firstname: first, lastname: last, mail: "#{login}@example.com", password: 'password123', password_confirmation: 'password123', language: 'en') end team = Group.create!(lastname: 'Developers') team.users << people[0..2] manager = Role.find_by!(name: 'Manager') developer = Role.find_by!(name: 'Developer') reporter = Role.find_by!(name: 'Reporter') customer_field = IssueCustomField.create!(name: 'Customer', field_format: 'list', possible_values: %w[Acme Globex Initech], is_for_all: true, is_filter: true, tracker_ids: Tracker.ids) review_field = IssueCustomField.create!(name: 'Review date', field_format: 'date', is_for_all: true, is_filter: true, tracker_ids: Tracker.ids) modules = Redmine::AccessControl.available_project_modules.map(&:to_s) make_project = lambda do |name, identifier, parent, description| Project.create!(name: name, identifier: identifier, description: description, is_public: true, parent: parent, tracker_ids: Tracker.ids, enabled_module_names: modules) end apollo = make_project.('Apollo', 'apollo', nil, "Customer portal relaunch.\n\n**Goal:** ship v2.0 this quarter.") web = make_project.('Apollo Web', 'apollo-web', apollo, 'Frontend of the customer portal.') api = make_project.('Apollo API', 'apollo-api', apollo, 'REST backend.') infra = make_project.('Infrastructure', 'infra', nil, 'Servers, CI and monitoring.') projects = [apollo, web, api, infra] projects.each do |p| Member.create!(project: p, principal: admin, role_ids: [manager.id]) Member.create!(project: p, principal: people[0], role_ids: [manager.id]) Member.create!(project: p, principal: team, role_ids: [developer.id]) Member.create!(project: p, principal: people[3], role_ids: [reporter.id]) %w[Frontend Backend Docs].each { |c| IssueCategory.create!(project: p, name: c) } end v1 = Version.create!(project: apollo, name: '1.0', effective_date: today - 60, sharing: 'descendants') v2 = Version.create!(project: apollo, name: '2.0', effective_date: today + 14, sharing: 'descendants', description: 'Portal relaunch') v3 = Version.create!(project: apollo, name: '2.1', effective_date: today + 60, sharing: 'descendants') vi = Version.create!(project: infra, name: 'Q4 hardening', effective_date: today + 30) titles = [ 'Login page shows wrong error message', 'Add CSV export to reports', 'Upgrade database driver', 'Dashboard loads slowly with many widgets', 'Password reset mail not localized', 'Add dark mode', 'Refactor session handling', 'Document REST authentication', 'Search ignores umlauts', 'Broken layout on small screens', 'Add audit log', 'Rate limit API endpoints', 'Improve onboarding tour', 'Fix timezone in due dates', 'Add SSO via OIDC', 'Remove legacy XML endpoints', 'Paginate project list', 'Cache avatar images', 'Nightly backup verification', 'Migrate CI to new runners' ] open_statuses = IssueStatus.sorted.reject(&:is_closed?) closed_status = IssueStatus.sorted.find(&:is_closed?) priorities = IssuePriority.active.to_a assignees = people + [admin, nil] issues = Array.new(60) do |i| project = projects[i % projects.size] version = project == infra ? vi : [v1, v2, v2, v3, nil][i % 5] status = version == v1 || i % 9 == 7 ? closed_status : open_statuses[i % open_statuses.size] start = today + rand(-40..20) suffix = i >= titles.size ? " (#{i / titles.size + 1})" : '' issue = Issue.new(project: project, tracker: project.trackers.to_a[i % project.trackers.size], author: people[i % people.size], subject: titles[i % titles.size] + suffix, description: "Steps to reproduce:\n\n1. Open the page\n2. Click **Save**\n3. See the error\n\n```ruby\nputs 'hello'\n```", priority: priorities[i % priorities.size], assigned_to: assignees[i % assignees.size], category: project.issue_categories.to_a[i % 3], fixed_version: version, start_date: start, due_date: start + rand(3..30), estimated_hours: [2, 4, 8, 16, nil][i % 5], done_ratio: status.is_closed? ? 100 : [0, 10, 30, 50, 80][i % 5], is_private: i == 5) issue.status = status issue.custom_field_values = { customer_field.id => %w[Acme Globex Initech][i % 3], review_field.id => (today + i).to_s } issue.save! issue end v1.update!(status: 'closed') # closed versions reject new issues, so close it afterwards # Subtasks and relations (same project: cross-project relations are off by default) showcase = issues[12] # open apollo issue: gets subtasks, attachments and watchers [issues[24], issues[28]].each { |child| child.reload.update!(parent_issue_id: showcase.id) } IssueRelation.create!(issue_from: showcase, issue_to: issues[32], relation_type: 'relates') IssueRelation.create!(issue_from: issues[9], issue_to: issues[13], relation_type: 'blocks') IssueRelation.create!(issue_from: issues[14], issue_to: issues[18], relation_type: 'precedes') # Journals: notes and attribute changes issues.first(15).each_with_index do |issue, i| issue.reload.init_journal(people[i % 3], "Looked into this. #{i.even? ? 'Needs a decision from **product**.' : 'Fix is in review.'}") issue.done_ratio = [issue.done_ratio.to_i + 10, 100].min unless issue.closed? issue.save! end showcase.watcher_users << people[1..2] # Attachments # 160x90 solid-color RGB PNG, built by hand so the seed needs no image file png_chunk = ->(type, data) { [data.bytesize].pack('N') + type + data + [Zlib.crc32(type + data)].pack('N') } png = "\x89PNG\r\n\x1a\n".b + png_chunk.('IHDR', [160, 90, 8, 2, 0, 0, 0].pack('NNC5')) + png_chunk.('IDAT', Zlib::Deflate.deflate(([0] + [103, 80, 164] * 160).pack('C*') * 90)) + png_chunk.('IEND', '') [['screenshot.png', png, 'image/png'], ['notes.txt', "Some notes\n", 'text/plain']].each do |name, data, type| Attachment.create!(file: StringIO.new(data), author: admin, filename: name, content_type: type, container: showcase) end # Wiki wiki = apollo.reload.wiki start_text = <<~MD # Apollo wiki {{toc}} ## Overview The customer portal lets clients see **tickets**, *invoices* and `usage`. > Keep this page short. Details go into child pages. ## Team | Name | Role | Since | |------|------|-------| | Jane Doe | Product owner | 2024 | | Max Smith | Backend | 2025 | | Linh Nguyen | Frontend | 2026 | ## Setup ```ruby class Portal def initialize(user) @user = user # current user end end ``` - [ ] Install dependencies - [x] Configure database See [[Architecture]] and issue #13. MD page = WikiPage.new(wiki: wiki, title: wiki.start_page) page.content = WikiContent.new(page: page, text: start_text, author: admin) page.save! child = WikiPage.new(wiki: wiki, title: 'Architecture', parent: page) child.content = WikiContent.new(page: child, text: "# Architecture\n\nThree tiers: web, API, database.", author: people[1]) child.save! # News, forum, documents news = News.create!(project: apollo, title: 'Version 2.0 beta available', summary: 'Try the new portal', description: 'The beta is live on staging. Please report issues.', author: people[0]) Comment.create!(commented: news, author: people[1], content: 'Great work!') board = Board.create!(project: apollo, name: 'General', description: 'General discussion') topic = Message.create!(board: board, subject: 'Release planning', content: 'When do we freeze 2.0?', author: people[0]) Message.create!(board: board, parent: topic, subject: 'RE: Release planning', content: 'Next Friday.', author: people[1]) Document.create!(project: apollo, title: 'Requirements', description: 'Signed-off requirements.', category: DocumentCategory.first) # Time entries over the last 30 days activities = TimeEntryActivity.active.to_a issues.first(20).each_with_index do |issue, i| user = people[i % 3] TimeEntry.create!(project: issue.project, issue: issue, user: user, author: user, hours: [0.5, 1, 2, 3.5][i % 4], spent_on: today - (i % 30), activity: activities[i % activities.size], comments: 'Work on issue') end # Saved public query: grouped, with totals IssueQuery.create!(name: 'Open by assignee', user: admin, visibility: Query::VISIBILITY_PUBLIC, group_by: 'assigned_to', totalable_names: %w[estimated_hours spent_hours], column_names: %i[tracker status priority subject assigned_to due_date done_ratio]) end puts "Seeded #{Project.count} projects, #{Issue.count} issues, #{User.count} users." load File.join(__dir__, 'spread_activity.rb') load File.join(__dir__, 'seed_extras.rb')