202 lines
9.7 KiB
Ruby
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')
|