Files
redmine-theme-tonalmine/dev/seed.rb
T

202 lines
9.7 KiB
Ruby

# 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')