effective_classifieds 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +122 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/effective_classifieds_manifest.js +3 -0
  6. data/app/assets/javascripts/effective_classifieds/base.js +0 -0
  7. data/app/assets/javascripts/effective_classifieds.js +1 -0
  8. data/app/assets/stylesheets/effective_classifieds/base.scss +0 -0
  9. data/app/assets/stylesheets/effective_classifieds.scss +1 -0
  10. data/app/controllers/admin/classified_submissions_controller.rb +19 -0
  11. data/app/controllers/admin/classifieds_controller.rb +20 -0
  12. data/app/controllers/effective/classified_submissions_controller.rb +31 -0
  13. data/app/controllers/effective/classifieds_controller.rb +53 -0
  14. data/app/datatables/admin/effective_classified_submissions_datatable.rb +22 -0
  15. data/app/datatables/admin/effective_classifieds_datatable.rb +61 -0
  16. data/app/datatables/effective_classified_submissions_datatable.rb +37 -0
  17. data/app/datatables/effective_classifieds_datatable.rb +25 -0
  18. data/app/helpers/effective_classifieds_helper.rb +7 -0
  19. data/app/mailers/effective/classifieds_mailer.rb +22 -0
  20. data/app/models/concerns/effective_classifieds_classified_submission.rb +112 -0
  21. data/app/models/effective/classified.rb +164 -0
  22. data/app/models/effective/classified_submission.rb +7 -0
  23. data/app/views/admin/classifieds/_form.html.haml +12 -0
  24. data/app/views/admin/classifieds/_form_access.html.haml +10 -0
  25. data/app/views/admin/classifieds/_form_classified.html.haml +3 -0
  26. data/app/views/effective/classified_submissions/_classified.haml +12 -0
  27. data/app/views/effective/classified_submissions/_classified_submission.html.haml +8 -0
  28. data/app/views/effective/classified_submissions/_content.html.haml +1 -0
  29. data/app/views/effective/classified_submissions/_dashboard.html.haml +28 -0
  30. data/app/views/effective/classified_submissions/_layout.html.haml +3 -0
  31. data/app/views/effective/classified_submissions/_summary.html.haml +30 -0
  32. data/app/views/effective/classified_submissions/classified.html.haml +17 -0
  33. data/app/views/effective/classified_submissions/start.html.haml +16 -0
  34. data/app/views/effective/classified_submissions/submitted.html.haml +13 -0
  35. data/app/views/effective/classified_submissions/summary.html.haml +8 -0
  36. data/app/views/effective/classifieds/_classified.html.haml +50 -0
  37. data/app/views/effective/classifieds/_dashboard.html.haml +8 -0
  38. data/app/views/effective/classifieds/_fields.html.haml +28 -0
  39. data/app/views/effective/classifieds/_form.html.haml +3 -0
  40. data/app/views/effective/classifieds/_layout.html.haml +1 -0
  41. data/app/views/effective/classifieds/_spacer.html.haml +1 -0
  42. data/app/views/effective/classifieds/index.html.haml +8 -0
  43. data/app/views/effective/classifieds/show.html.haml +8 -0
  44. data/app/views/effective/classifieds_mailer/classified_submitted.html.haml +16 -0
  45. data/app/views/layouts/effective_classifieds_mailer_layout.html.haml +7 -0
  46. data/config/effective_classifieds.rb +45 -0
  47. data/config/routes.rb +24 -0
  48. data/db/migrate/01_create_effective_classifieds.rb.erb +62 -0
  49. data/db/seeds.rb +1 -0
  50. data/lib/effective_classifieds/engine.rb +18 -0
  51. data/lib/effective_classifieds/version.rb +3 -0
  52. data/lib/effective_classifieds.rb +45 -0
  53. data/lib/generators/effective_classifieds/install_generator.rb +31 -0
  54. data/lib/generators/templates/effective_classifieds_mailer_preview.rb +4 -0
  55. data/lib/tasks/effective_classifieds_tasks.rake +8 -0
  56. metadata +251 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b6ec3f5b89b358551558d22206f8ce57ef5002f199a635c9e004155a12ff246
4
+ data.tar.gz: 0c36fa15a0dd1ae22c8cd74fef90e8b28bd6bec2e74efe2950c3bb32ee24e999
5
+ SHA512:
6
+ metadata.gz: 1377dab4d35fc6f2a0c160f006246380cf3af0adabd97100e5b78c61d5c2402c274d747c7eee3ca4126317fe38efc648dc142a549e0be7e31e2f257d4bcbb937
7
+ data.tar.gz: 2043dfe2ce40bd423fe50c5c80b2c44be329d28c53504c820bb009188382ccb8cdce2596deaf8d374f97786a8cbf09270c8465cb8067edbdaea1a2a1ae0b440a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Code and Effect Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # Effective Classifieds
2
+
3
+ Submit classified ads for job openings and equipment sales.
4
+
5
+ ## Getting Started
6
+
7
+ This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.
8
+
9
+ Please first install the [effective_datatables](https://github.com/code-and-effect/effective_datatables) gem.
10
+
11
+ Please download and install the [Twitter Bootstrap4](http://getbootstrap.com)
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'haml-rails' # or try using gem 'hamlit-rails'
17
+ gem 'effective_classifieds'
18
+ ```
19
+
20
+ Run the bundle command to install it:
21
+
22
+ ```console
23
+ bundle install
24
+ ```
25
+
26
+ Then run the generator:
27
+
28
+ ```ruby
29
+ rails generate effective_classifieds:install
30
+ ```
31
+
32
+ The generator will install an initializer which describes all configuration options and creates a database migration.
33
+
34
+ If you want to tweak the table names, manually adjust both the configuration file and the migration now.
35
+
36
+ Then migrate the database:
37
+
38
+ ```ruby
39
+ rake db:migrate
40
+ ```
41
+
42
+ Please add the following to your User model:
43
+
44
+ ```
45
+ effective_classifieds_user
46
+
47
+ Use the following datatables to display to your user their applicants dues:
48
+
49
+ ```haml
50
+ %h2 My Classifieds
51
+ - datatable = EffectiveClassifiedsDatatable.new(self)
52
+ ```
53
+
54
+ and
55
+
56
+ ```
57
+ Add a link to the admin menu:
58
+
59
+ ```haml
60
+ - if can? :admin, :effective_classifieds
61
+ - if can? :index, Effective::Classified
62
+ = nav_link_to 'Classifieds', effective_classifieds.admin_classifieds_path
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ ## Authorization
68
+
69
+ All authorization checks are handled via the effective_resources gem found in the `config/initializers/effective_resources.rb` file.
70
+
71
+ ## Effective Roles
72
+
73
+ This gem works with effective roles for the representative roles.
74
+
75
+ Configure your `config/initializers/effective_roles.rb` something like this:
76
+
77
+ ```
78
+ ```
79
+
80
+ ## Permissions
81
+
82
+ The permissions you actually want to define are as follows (using CanCan):
83
+
84
+ ```ruby
85
+ can([:index, :show], Effective::Classified) { |classified| classified.published? }
86
+ can([:show, :edit, :update], Effective::Classified) { |classified| classified.owner == user }
87
+
88
+ can([:show, :index, :destroy], EffectiveClassifieds.ClassifiedSubmission) { |submission| submission.owner == user }
89
+ can([:update], EffectiveClassifieds.ClassifiedSubmission) { |submission| submission.owner == user && !submission.was_submitted? }
90
+
91
+ if user.admin?
92
+ can :admin, :effective_classifieds
93
+
94
+ can(crud - [:destroy], Classified)
95
+
96
+ can(:approve, Classified) { |classified| classified.was_submitted? && !classified.approved? }
97
+ can(:destroy, Classified) { |classified| !classified.draft? }
98
+
99
+ can([:new, :index, :show], EffectiveClassifieds.ClassifiedSubmission)
100
+ end
101
+ ```
102
+
103
+ ## License
104
+
105
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
106
+
107
+ ## Testing
108
+
109
+ Run tests by:
110
+
111
+ ```ruby
112
+ rails test
113
+ ```
114
+
115
+ ## Contributing
116
+
117
+ 1. Fork it
118
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
119
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
120
+ 4. Push to the branch (`git push origin my-new-feature`)
121
+ 5. Bonus points for test coverage
122
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,3 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
3
+ //= link_tree ../images
@@ -0,0 +1 @@
1
+ //= require_tree ./effective_classifieds
@@ -0,0 +1 @@
1
+ @import 'effective_classifieds/base';
@@ -0,0 +1,19 @@
1
+ module Admin
2
+ class ClassifiedSubmissionsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_classifieds) }
5
+
6
+ include Effective::CrudController
7
+
8
+ resource_scope -> { EffectiveEvents.ClassifiedSubmission.deep.all }
9
+ datatable -> { Admin::EffectiveClassifiedSubmissionsDatatable.new }
10
+
11
+ private
12
+
13
+ def permitted_params
14
+ model = (params.key?(:effective_classified_submission) ? :effective_classified_submission : :classified_submission)
15
+ params.require(model).permit!
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Admin
2
+ class ClassifiedsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_classifieds) }
5
+
6
+ include Effective::CrudController
7
+
8
+ submit :save, 'Save'
9
+ submit :save, 'Save and View', redirect: -> { effective_classifieds.classified_path(resource) }
10
+ submit :approve, 'Approve'
11
+
12
+ private
13
+
14
+ def permitted_params
15
+ model = (params.key?(:effective_classified) ? :effective_classified : :classified)
16
+ params.require(model).permit!
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ module Effective
2
+ class ClassifiedSubmissionsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+
5
+ include Effective::WizardController
6
+
7
+ resource_scope -> { EffectiveClassifieds.ClassifiedSubmission.deep.where(owner: current_user) }
8
+
9
+ # Allow only 1 in-progress application at a time
10
+ before_action(only: [:new, :show], unless: -> { resource&.done? }) do
11
+ existing = resource_scope.in_progress.where.not(id: resource).first
12
+
13
+ if existing.present?
14
+ flash[:success] = "You have been redirected to your existing in progress classified submission"
15
+ redirect_to effective_classifieds.classified_submission_build_path(existing, existing.next_step)
16
+ end
17
+ end
18
+
19
+ after_save do
20
+ flash.now[:success] = ''
21
+ end
22
+
23
+ private
24
+
25
+ def permitted_params
26
+ model = (params.key?(:effective_classified_submission) ? :effective_classified_submission : :classified_submission)
27
+ params.require(model).permit!.except(:status, :status_steps, :wizard_steps, :submitted_at)
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ module Effective
2
+ class ClassifiedsController < ApplicationController
3
+ include Effective::CrudController
4
+
5
+ resource_scope -> {
6
+ unpublished = EffectiveResources.authorized?(self, :admin, :effective_classifieds)
7
+ Effective::Classified.classifieds(user: current_user, unpublished: unpublished)
8
+ }
9
+
10
+ def index
11
+ @classifieds ||= resource_scope.published
12
+
13
+ @classifieds = @classifieds.paginate(page: params[:page])
14
+
15
+ # if params[:search].present?
16
+ # search = params[:search].permit(EffectiveClassifieds.permitted_params).delete_if { |k, v| v.blank? }
17
+ # @classifieds = @classifieds.where(search) if search.present?
18
+ # end
19
+
20
+ EffectiveResources.authorize!(self, :index, Effective::Classified)
21
+
22
+ @page_title ||= ['Classifieds', (" - Page #{params[:page]}" if params[:page])].compact.join
23
+ end
24
+
25
+ def show
26
+ @classified = resource_scope.find(params[:id])
27
+
28
+ if @classified.respond_to?(:roles_permit?)
29
+ raise Effective::AccessDenied.new('Access Denied', :show, @classified) unless @classified.roles_permit?(current_user)
30
+ end
31
+
32
+ EffectiveResources.authorize!(self, :show, @classified)
33
+
34
+ if EffectiveResources.authorized?(self, :admin, :effective_classifieds)
35
+ flash.now[:warning] = [
36
+ 'Hi Admin!',
37
+ ('You are viewing a hidden classified.' unless @classified.published?),
38
+ 'Click here to',
39
+ ("<a href='#{effective_classifieds.edit_admin_classified_path(@classified)}' class='alert-link'>edit classified settings</a>.")
40
+ ].compact.join(' ')
41
+ end
42
+
43
+ @page_title ||= @classified.to_s
44
+ end
45
+
46
+ private
47
+
48
+ def permitted_params
49
+ params.require(:effective_classified).permit!.except(:status, :status_steps)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,22 @@
1
+ class Admin::EffectiveClassifiedSubmissionsDatatable < Effective::Datatable
2
+ datatable do
3
+ order :created_at
4
+
5
+ col :token, visible: false
6
+
7
+ col :created_at, label: 'Created', visible: false
8
+ col :updated_at, label: 'Updated', visible: false
9
+
10
+ col :submitted_at, label: 'Submitted', visible: false, as: :date
11
+
12
+ col :classified, search: :string
13
+ col :owner
14
+
15
+ actions_col
16
+ end
17
+
18
+ collection do
19
+ EffectiveSubmissions.ClassifiedSubmission.all.deep.done.joins(:classified)
20
+ end
21
+
22
+ end
@@ -0,0 +1,61 @@
1
+ module Admin
2
+ class EffectiveClassifiedsDatatable < Effective::Datatable
3
+ filters do
4
+ scope :all
5
+
6
+ unless EffectiveClassifieds.auto_approve
7
+ scope :submitted
8
+ scope :approved
9
+ end
10
+
11
+ scope :published
12
+ end
13
+
14
+ datatable do
15
+
16
+ col :updated_at, visible: false
17
+ col :created_at, visible: false
18
+
19
+ col :id, visible: false
20
+
21
+ col :classified_submission, visible: false, search: :string
22
+
23
+ col :start_on, as: :date
24
+ col :end_on, as: :date
25
+
26
+ if categories.present?
27
+ col :category, search: categories
28
+ end
29
+
30
+ col :title
31
+ col :body
32
+ col :slug, visible: false
33
+
34
+ col :organization
35
+ col :location
36
+
37
+ col :website
38
+ col :email
39
+ col :phone
40
+
41
+ col :archived
42
+
43
+ unless EffectiveClassifieds.auto_approve
44
+ col :status, search: ['submitted', 'approved']
45
+ end
46
+
47
+ actions_col do |classified|
48
+ dropdown_link_to('View Classified', effective_classifieds.classified_path(classified), target: '_blank')
49
+ end
50
+ end
51
+
52
+ collection do
53
+ Effective::Classified.all.where.not(status: :draft)
54
+ end
55
+
56
+ def categories
57
+ EffectiveClassifieds.categories
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ # Dashboard Classified Submissions
2
+ class EffectiveClassifiedSubmissionsDatatable < Effective::Datatable
3
+ datatable do
4
+ order :created_at
5
+
6
+ col :token, visible: false
7
+ col :created_at, visible: false
8
+
9
+ col :submitted_at do |submission|
10
+ submission.submitted_at&.strftime('%F') || 'Incomplete'
11
+ end
12
+
13
+ col :classified, search: :string
14
+
15
+ col :owner, visible: false, search: :string
16
+
17
+ col :status do |submission|
18
+ submission.classified&.status || submission.status
19
+ end
20
+
21
+ actions_col(actions: []) do |submission|
22
+ if submission.draft?
23
+ dropdown_link_to('Continue', effective_classifieds.classified_submission_build_path(submission, submission.next_step), 'data-turbolinks' => false)
24
+ else
25
+ dropdown_link_to('Show', effective_classifieds.classified_submission_path(submission))
26
+ dropdown_link_to('Edit', effective_classifieds.edit_classified_path(submission.classified)) if submission.classified
27
+ end
28
+
29
+ dropdown_link_to('Delete', effective_classifieds.classified_submission_path(submission), 'data-confirm': "Really delete #{submission}?", 'data-method': :delete)
30
+ end
31
+ end
32
+
33
+ collection do
34
+ EffectiveClassifieds.ClassifiedSubmission.deep.where(owner: current_user).left_joins(:classified)
35
+ end
36
+
37
+ end
@@ -0,0 +1,25 @@
1
+ # Dashboard Classifieds
2
+ class EffectiveClassifiedsDatatable < Effective::Datatable
3
+ datatable do
4
+ order :start_on
5
+
6
+ col :start_on, as: :date, label: 'Starts'
7
+ col :end_on, as: :date, label: 'Ends'
8
+
9
+ col :title
10
+ col :organization
11
+ col :location
12
+
13
+ col :body, visible: false
14
+ col :website, visible: false
15
+ col :email, visible: false
16
+ col :phone, visible: false
17
+
18
+ actions_col(edit: false)
19
+ end
20
+
21
+ collection do
22
+ Effective::Classified.classifieds(user: current_user)
23
+ end
24
+
25
+ end
@@ -0,0 +1,7 @@
1
+ module EffectiveClassifiedsHelper
2
+
3
+ def edit_effective_classified_submissions_wizard?
4
+ params[:controller] == 'effective/classified_submissions' && defined?(resource) && resource.draft?
5
+ end
6
+
7
+ end
@@ -0,0 +1,22 @@
1
+ module Effective
2
+ class ClassifiedsMailer < EffectiveClassifieds.parent_mailer_class
3
+
4
+ default from: -> { EffectiveClassifieds.mailer_sender }
5
+ layout -> { EffectiveClassifieds.mailer_layout || 'effective_classifieds_mailer_layout' }
6
+
7
+ def classified_submitted(resource, opts = {})
8
+ raise('expected an Effective::Classification') unless resource.kind_of?(Effective::Classified)
9
+
10
+ @classified = resource
11
+
12
+ mail(to: EffectiveClassifieds.mailer_admin, **headers_for(resource, opts))
13
+ end
14
+
15
+ protected
16
+
17
+ def headers_for(resource, opts = {})
18
+ resource.respond_to?(:log_changes_datatable) ? opts.merge(log: resource) : opts
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EffectiveClassifiedsClassifiedSubmission
4
+ #
5
+ # Mark your owner model with effective_classifieds_classified_submission to get all the includes
6
+
7
+ module EffectiveClassifiedsClassifiedSubmission
8
+ extend ActiveSupport::Concern
9
+
10
+ module Base
11
+ def effective_classifieds_classified_submission
12
+ include ::EffectiveClassifiedsClassifiedSubmission
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def effective_classifieds_classified_submission?; true; end
18
+
19
+ def all_wizard_steps
20
+ const_get(:WIZARD_STEPS).keys
21
+ end
22
+
23
+ def required_wizard_steps
24
+ [:start, :summary, :submitted]
25
+ end
26
+ end
27
+
28
+ included do
29
+ acts_as_tokened
30
+
31
+ acts_as_statused(
32
+ :draft, # Just Started
33
+ :submitted # All done
34
+ )
35
+
36
+ acts_as_wizard(
37
+ start: 'Start',
38
+ classified: 'Classified',
39
+ summary: 'Review',
40
+ submitted: 'Submitted'
41
+ )
42
+
43
+ log_changes(except: :wizard_steps) if respond_to?(:log_changes)
44
+
45
+ # Application Namespace
46
+ belongs_to :owner, polymorphic: true
47
+ accepts_nested_attributes_for :owner
48
+
49
+ # Effective Namespace
50
+ has_one :classified, class_name: 'Effective::Classified', inverse_of: :classified_submission, dependent: :destroy
51
+ accepts_nested_attributes_for :classified, reject_if: :all_blank, allow_destroy: true
52
+
53
+ effective_resource do
54
+ # Acts as Statused
55
+ status :string, permitted: false
56
+ status_steps :text, permitted: false
57
+
58
+ # Dates
59
+ submitted_at :datetime
60
+
61
+ # Acts as Wizard
62
+ wizard_steps :text, permitted: false
63
+
64
+ timestamps
65
+ end
66
+
67
+ scope :deep, -> { includes(:classified) }
68
+ scope :sorted, -> { order(:id) }
69
+
70
+ scope :in_progress, -> { where.not(status: [:submitted]) }
71
+ scope :done, -> { where(status: [:submitted]) }
72
+
73
+ scope :for, -> (user) { where(owner: user) }
74
+
75
+ # All Steps validations
76
+ validates :owner, presence: true
77
+
78
+ # Classified Step
79
+ with_options(if: -> { current_step == :classified }) do
80
+ validates :classified, presence: true
81
+ end
82
+ end
83
+
84
+ # Instance Methods
85
+ def to_s
86
+ 'classified submission'
87
+ end
88
+
89
+ def in_progress?
90
+ draft?
91
+ end
92
+
93
+ def done?
94
+ submitted?
95
+ end
96
+
97
+ # Called on the Review / Summary Step
98
+ # But we actually want to submit it
99
+ def summary!
100
+ submit!
101
+ end
102
+
103
+ def submit!
104
+ raise('already submitted') if was_submitted?
105
+
106
+ classified.submit! unless classified.was_submitted?
107
+
108
+ wizard_steps[:submitted] = Time.zone.now
109
+ submitted!
110
+ end
111
+
112
+ end