2929#++
3030
3131class Project < ApplicationRecord
32- extend FriendlyId
33-
3432 include Projects ::Activity
3533 include Projects ::AncestorsFromRoot
3634 include Projects ::CustomFields
@@ -40,16 +38,10 @@ class Project < ApplicationRecord
4038 include Projects ::Versions
4139 include Projects ::WorkPackageCustomFields
4240 include Projects ::CreationWizard
41+ include Projects ::Identifier
4342
4443 include ::Scopes ::Scoped
4544
46- # Maximum length for project identifiers
47- IDENTIFIER_MAX_LENGTH = 100
48- SEMANTIC_IDENTIFIER_MAX_LENGTH = 10
49-
50- # reserved identifiers
51- RESERVED_IDENTIFIERS = %w[ new menu queries filters identifier_update_dialog identifier_suggestion ] . freeze
52-
5345 enum :workspace_type , {
5446 project : "project" ,
5547 program : "program" ,
@@ -135,25 +127,6 @@ class Project < ApplicationRecord
135127 # extended in Projects::CustomFields in order to support sections
136128 # and project-level activation of custom fields
137129
138- # Override the `validation_context` getter to include the `default_validation_context` when the
139- # context is `:saving_custom_fields`. This is required, because the `acts_as_url` plugin from
140- # `stringex` defines a callback on the `:create` context for initialising the `identifier` field.
141- # Providing a custom context while creating the project, will not execute the callbacks on the
142- # `:create` or `:update` contexts, meaning the identifier will not get initialised.
143- # In order to initialise the identifier, the `default_validation_context` (`:create`, or `:update`)
144- # should be included when validating via the `:saving_custom_fields`. This way every create
145- # or update callback will also be executed alongside the `:saving_custom_fields` callbacks.
146- # This problem does not affect the contextless callbacks, they are always executed.
147-
148- def validation_context
149- case Array ( super )
150- in [ *, :saving_custom_fields , *] => context
151- context | [ default_validation_context ]
152- else
153- super
154- end
155- end
156-
157130 acts_as_searchable columns : %W( #{ table_name } .name #{ table_name } .identifier #{ table_name } .description) ,
158131 date_column : "#{ table_name } .created_at" ,
159132 project_key : "id" ,
@@ -193,53 +166,8 @@ def validation_context
193166 # neither development nor deployment setups are prepared for this
194167 # validates_presence_of :types
195168
196- acts_as_url :name ,
197- url_attribute : :identifier ,
198- sync_url : false , # Don't update identifier when name changes
199- only_when_blank : true , # Only generate when identifier not set
200- limit : IDENTIFIER_MAX_LENGTH ,
201- blacklist : RESERVED_IDENTIFIERS ,
202- adapter : OpenProject ::ActsAsUrl ::Adapter ::OpActiveRecord # use a custom adapter able to handle edge cases
203-
204- ### Validators for the legacy underscored identifier format (e.g. "project_one")
205- validates :identifier ,
206- presence : true ,
207- uniqueness : { case_sensitive : true } ,
208- length : { maximum : IDENTIFIER_MAX_LENGTH } ,
209- exclusion : RESERVED_IDENTIFIERS ,
210- if : -> ( p ) { p . persisted? || p . identifier . present? }
211- # Contains only a-z, 0-9, dashes and underscores but cannot consist of numbers only as it would clash with the id.
212- validates :identifier ,
213- format : { with : /\A (?!^\d +\z )[a-z0-9\- _]+\z / } ,
214- if : -> ( p ) {
215- p . identifier_changed? && p . identifier . present? && !Setting ::WorkPackageIdentifier . alphanumeric?
216- }
217-
218- ### Validators for the uppercase identifier format (e.g. "PROJ1")
219- validates :identifier ,
220- format : { with : /\A [A-Z]/ , message : :must_start_with_letter } ,
221- if : -> ( p ) { p . identifier_changed? && p . identifier . present? && Setting ::WorkPackageIdentifier . alphanumeric? }
222- validates :identifier ,
223- format : { with : /\A [A-Z][A-Z0-9_]*\z / , message : :no_special_characters } ,
224- length : { maximum : SEMANTIC_IDENTIFIER_MAX_LENGTH } ,
225- if : -> ( p ) { p . identifier_changed? && p . identifier . present? && Setting ::WorkPackageIdentifier . alphanumeric? }
226-
227- # Complements the uniqueness validation above: once an identifier has been used by a
228- # project, it remains reserved for that project even after the project moves to a new
229- # identifier. This prevents another project from claiming a "retired" identifier.
230- validate :identifier_not_historically_reserved , if : -> ( p ) { p . identifier_changed? }
231-
232169 validates_associated :repository , :wiki
233170
234- friendly_id :identifier , use : %i[ finders history ] , slug_column : :identifier
235-
236- # FriendlyId::Slugged adds after_validation :unset_slug_if_invalid, which reverts the
237- # slug column to its previous value when validation fails. With slug_column: :identifier,
238- # this would reset a manually-set identifier back to nil on new records. Since the
239- # identifier is managed by acts_as_url and user input (not FriendlyId's slug generator),
240- # we disable this behaviour entirely.
241- def unset_slug_if_invalid ; end
242-
243171 scopes :activated_in_storage ,
244172 :allowed_to ,
245173 :assignable_parents ,
@@ -291,14 +219,6 @@ def copy_allowed?
291219 User . current . allowed_in_project? ( :copy_projects , self )
292220 end
293221
294- def self . suggest_identifier ( name )
295- if Setting ::WorkPackageIdentifier . alphanumeric?
296- WorkPackages ::IdentifierAutofix ::ProjectIdentifierSuggestionGenerator . suggest_identifier ( name )
297- else # This should closely enough emulate Project models' usage of acts_as_url
298- name . to_url . first ( IDENTIFIER_MAX_LENGTH ) . presence || "project"
299- end
300- end
301-
302222 def self . selectable_projects
303223 Project . visible . select { |p | User . current . member_of? p } . sort_by ( &:to_s )
304224 end
@@ -388,20 +308,4 @@ def module_disabled(disabled_module)
388308 OpenProject ::Events ::MODULE_DISABLED , disabled_module :
389309 )
390310 end
391-
392- private
393-
394- # Checks friendly_id_slugs for any project that previously used this identifier and
395- # has since changed it. It allows to switch back to an identifier the project itself
396- # has used before.
397- def identifier_not_historically_reserved
398- return if errors . any? { |error | error . attribute == :identifier && error . type == :taken }
399-
400- already_existing = FriendlyId ::Slug
401- . where ( slug : identifier , sluggable_type : self . class . to_s )
402- . where . not ( sluggable_id : id )
403- . exists?
404-
405- errors . add ( :identifier , :taken ) if already_existing
406- end
407311end
0 commit comments