sufia-models 4.3.1 → 5.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/app/actors/sufia/generic_file/actor.rb +7 -10
  3. data/app/jobs/active_fedora_pid_based_job.rb +2 -3
  4. data/app/jobs/audit_job.rb +28 -31
  5. data/app/jobs/batch_update_job.rb +9 -8
  6. data/app/jobs/import_url_job.rb +2 -2
  7. data/app/models/batch.rb +11 -12
  8. data/app/models/checksum_audit_log.rb +7 -8
  9. data/app/models/concerns/sufia/ability.rb +4 -6
  10. data/app/models/concerns/sufia/collection.rb +4 -5
  11. data/app/models/concerns/sufia/file_stat_utils.rb +3 -3
  12. data/app/models/concerns/sufia/generic_file.rb +16 -14
  13. data/app/models/concerns/sufia/generic_file/audit.rb +50 -31
  14. data/app/models/concerns/sufia/generic_file/characterization.rb +3 -3
  15. data/app/models/concerns/sufia/generic_file/derivatives.rb +5 -5
  16. data/app/models/concerns/sufia/generic_file/full_text_indexing.rb +2 -2
  17. data/app/models/concerns/sufia/generic_file/metadata.rb +82 -11
  18. data/app/models/concerns/sufia/generic_file/proxy_deposit.rb +12 -3
  19. data/app/models/concerns/sufia/generic_file/versions.rb +1 -4
  20. data/app/models/concerns/sufia/generic_file/web_form.rb +13 -6
  21. data/app/models/concerns/sufia/model_methods.rb +11 -9
  22. data/app/models/concerns/sufia/user.rb +11 -28
  23. data/app/models/datastreams/file_content_datastream.rb +1 -1
  24. data/app/models/datastreams/fits_datastream.rb +1 -1
  25. data/app/models/file_download_stat.rb +2 -2
  26. data/app/models/file_usage.rb +5 -9
  27. data/app/models/file_view_stat.rb +2 -2
  28. data/app/models/local_authority.rb +2 -2
  29. data/app/models/proxy_deposit_request.rb +1 -1
  30. data/app/services/sufia/id_service.rb +5 -5
  31. data/app/services/sufia/noid.rb +10 -7
  32. data/lib/generators/sufia/models/cached_stats_generator.rb +31 -2
  33. data/lib/generators/sufia/models/install_generator.rb +31 -11
  34. data/lib/generators/sufia/models/proxies_generator.rb +31 -2
  35. data/lib/generators/sufia/models/templates/config/sufia.rb +10 -3
  36. data/lib/generators/sufia/models/upgrade400_generator.rb +33 -2
  37. data/lib/sufia/models/engine.rb +13 -4
  38. data/lib/sufia/models/file_content/versions.rb +12 -8
  39. data/lib/sufia/models/version.rb +1 -1
  40. data/lib/sufia/permissions/writable.rb +34 -16
  41. data/sufia-models.gemspec +4 -2
  42. metadata +91 -79
  43. data/app/models/concerns/sufia/generic_file/reload_on_save.rb +0 -18
  44. data/app/models/concerns/sufia/properties_datastream_behavior.rb +0 -32
  45. data/app/models/concerns/sufia/user_usage_stats.rb +0 -15
  46. data/app/models/datastreams/batch_rdf_datastream.rb +0 -6
  47. data/app/models/datastreams/generic_file_rdf_datastream.rb +0 -69
  48. data/app/models/datastreams/paranoid_rights_datastream.rb +0 -22
  49. data/app/models/datastreams/properties_datastream.rb +0 -4
  50. data/app/models/sufia/orcid_validator.rb +0 -8
  51. data/app/models/user_stat.rb +0 -2
  52. data/lib/generators/sufia/models/abstract_migration_generator.rb +0 -30
  53. data/lib/generators/sufia/models/orcid_field_generator.rb +0 -19
  54. data/lib/generators/sufia/models/templates/migrations/add_orcid_to_users.rb +0 -5
  55. data/lib/generators/sufia/models/templates/migrations/create_user_stats.rb +0 -19
  56. data/lib/generators/sufia/models/user_stats_generator.rb +0 -31
  57. data/lib/sufia/models/stats/user_stat_importer.rb +0 -85
  58. data/lib/tasks/stats_tasks.rake +0 -12
@@ -3,7 +3,7 @@ module Sufia
3
3
  module Characterization
4
4
  extend ActiveSupport::Concern
5
5
  included do
6
- has_metadata "characterization", type: FitsDatastream
6
+ contains "characterization", class_name: 'FitsDatastream'
7
7
  has_attributes :mime_type, datastream: :characterization, multiple: false
8
8
  has_attributes :format_label, :file_size, :last_modified,
9
9
  :filename, :original_checksum, :rights_basis,
@@ -45,11 +45,11 @@ module Sufia
45
45
  metadata = content.extract_metadata
46
46
  characterization.ng_xml = metadata if metadata.present?
47
47
  append_metadata
48
- self.filename = [self.label]
48
+ self.filename = [content.original_name]
49
49
  save
50
50
  end
51
51
 
52
- # Populate descMetadata with fields from FITS (e.g. Author from pdfs)
52
+ # Populate GenericFile's properties with fields from FITS (e.g. Author from pdfs)
53
53
  def append_metadata
54
54
  terms = self.characterization_terms
55
55
  Sufia.config.fits_to_desc_mapping.each_pair do |k, v|
@@ -9,15 +9,15 @@ module Sufia
9
9
  makes_derivatives do |obj|
10
10
  case obj.mime_type
11
11
  when *pdf_mime_types
12
- obj.transform_datastream :content, { thumbnail: { format: 'jpg', size: '338x493', datastream: 'thumbnail' } }
12
+ obj.transform_file :content, { thumbnail: { format: 'jpg', size: '338x493', datastream: 'thumbnail' } }
13
13
  when *office_document_mime_types
14
- obj.transform_datastream :content, { thumbnail: { format: 'jpg', size: '200x150>', datastream: 'thumbnail' } }, processor: :document
14
+ obj.transform_file :content, { thumbnail: { format: 'jpg', size: '200x150>', datastream: 'thumbnail' } }, processor: :document
15
15
  when *audio_mime_types
16
- obj.transform_datastream :content, { mp3: { format: 'mp3', datastream: 'mp3' }, ogg: { format: 'ogg', datastream: 'ogg' } }, processor: :audio
16
+ obj.transform_file :content, { mp3: { format: 'mp3', datastream: 'mp3' }, ogg: { format: 'ogg', datastream: 'ogg' } }, processor: :audio
17
17
  when *video_mime_types
18
- obj.transform_datastream :content, { webm: { format: 'webm', datastream: 'webm' }, mp4: { format: 'mp4', datastream: 'mp4' }, thumbnail: { format: 'jpg', datastream: 'thumbnail' } }, processor: :video
18
+ obj.transform_file :content, { webm: { format: 'webm', datastream: 'webm' }, mp4: { format: 'mp4', datastream: 'mp4' }, thumbnail: { format: 'jpg', datastream: 'thumbnail' } }, processor: :video
19
19
  when *image_mime_types
20
- obj.transform_datastream :content, { thumbnail: { format: 'jpg', size: '200x150>', datastream: 'thumbnail' } }
20
+ obj.transform_file :content, { thumbnail: { format: 'jpg', size: '200x150>', datastream: 'thumbnail' } }
21
21
  end
22
22
  end
23
23
  end
@@ -4,7 +4,7 @@ module Sufia
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- has_file_datastream 'full_text', versionable: false
7
+ contains 'full_text'
8
8
  end
9
9
 
10
10
  def append_metadata
@@ -27,7 +27,7 @@ module Sufia
27
27
  extracted_text = JSON.parse(resp.body)[''].rstrip
28
28
  full_text.content = extracted_text if extracted_text.present?
29
29
  rescue => e
30
- logger.error("Error extracting content from #{self.pid}: #{e.inspect}")
30
+ logger.error("Error extracting content from #{self.id}: #{e.inspect}")
31
31
  end
32
32
  end
33
33
  end
@@ -4,17 +4,88 @@ module Sufia
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- has_metadata "descMetadata", type: GenericFileRdfDatastream
8
- has_metadata "properties", type: PropertiesDatastream
9
- has_file_datastream "content", type: FileContentDatastream
10
- has_file_datastream "thumbnail"
11
-
12
- has_attributes :relative_path, :depositor, :import_url, datastream: :properties, multiple: false
13
- has_attributes :date_uploaded, :date_modified, datastream: :descMetadata, multiple: false
14
- has_attributes :related_url, :based_near, :part_of, :creator,
15
- :contributor, :title, :tag, :description, :rights,
16
- :publisher, :date_created, :subject,
17
- :resource_type, :identifier, :language, datastream: :descMetadata, multiple: true
7
+ contains "content", class_name: 'FileContentDatastream'
8
+ contains "thumbnail"
9
+
10
+ property :label, predicate: ::RDF::DC.title, multiple: false
11
+
12
+ property :depositor, predicate: ::RDF::URI.new("http://id.loc.gov/vocabulary/relators/dpt"), multiple: false do |index|
13
+ index.as :symbol, :stored_searchable
14
+ end
15
+
16
+ property :relative_path, predicate: ::RDF::URI.new('http://scholarsphere.psu.edu/ns#relativePath'), multiple: false
17
+
18
+ property :import_url, predicate: ::RDF::URI.new('http://scholarsphere.psu.edu/ns#importUrl'), multiple: false do |index|
19
+ index.as :symbol
20
+ end
21
+
22
+ property :part_of, predicate: ::RDF::DC.isPartOf
23
+ property :resource_type, predicate: ::RDF::DC.type do |index|
24
+ index.as :stored_searchable, :facetable
25
+ end
26
+ property :title, predicate: ::RDF::DC.title do |index|
27
+ index.as :stored_searchable, :facetable
28
+ end
29
+ property :creator, predicate: ::RDF::DC.creator do |index|
30
+ index.as :stored_searchable, :facetable
31
+ end
32
+ property :contributor, predicate: ::RDF::DC.contributor do |index|
33
+ index.as :stored_searchable, :facetable
34
+ end
35
+ property :description, predicate: ::RDF::DC.description do |index|
36
+ index.type :text
37
+ index.as :stored_searchable
38
+ end
39
+ property :tag, predicate: ::RDF::DC.relation do |index|
40
+ index.as :stored_searchable, :facetable
41
+ end
42
+ property :rights, predicate: ::RDF::DC.rights do |index|
43
+ index.as :stored_searchable
44
+ end
45
+ property :publisher, predicate: ::RDF::DC.publisher do |index|
46
+ index.as :stored_searchable, :facetable
47
+ end
48
+ property :date_created, predicate: ::RDF::DC.created do |index|
49
+ index.as :stored_searchable
50
+ end
51
+ property :date_uploaded, predicate: ::RDF::DC.dateSubmitted, multiple: false do |index|
52
+ index.type :date
53
+ index.as :stored_sortable
54
+ end
55
+ property :date_modified, predicate: ::RDF::DC.modified, multiple: false do |index|
56
+ index.type :date
57
+ index.as :stored_sortable
58
+ end
59
+ property :subject, predicate: ::RDF::DC.subject do |index|
60
+ index.as :stored_searchable, :facetable
61
+ end
62
+ property :language, predicate: ::RDF::DC.language do |index|
63
+ index.as :stored_searchable, :facetable
64
+ end
65
+ property :identifier, predicate: ::RDF::DC.identifier do |index|
66
+ index.as :stored_searchable
67
+ end
68
+ property :based_near, predicate: ::RDF::FOAF.based_near do |index|
69
+ index.as :stored_searchable, :facetable
70
+ end
71
+ property :related_url, predicate: ::RDF::RDFS.seeAlso do |index|
72
+ index.as :stored_searchable
73
+ end
74
+ property :bibliographic_citation, predicate: ::RDF::DC.bibliographicCitation do |index|
75
+ index.as :stored_searchable
76
+ end
77
+ property :source, predicate: ::RDF::DC.source do |index|
78
+ index.as :stored_searchable
79
+ end
80
+
81
+ # TODO: Move this somewhere more appropriate
82
+ begin
83
+ LocalAuthority.register_vocabulary(self, "subject", "lc_subjects")
84
+ LocalAuthority.register_vocabulary(self, "language", "lexvo_languages")
85
+ LocalAuthority.register_vocabulary(self, "tag", "lc_genres")
86
+ rescue
87
+ puts "tables for vocabularies missing"
88
+ end
18
89
  end
19
90
 
20
91
  # Add a schema.org itemtype
@@ -4,18 +4,27 @@ module Sufia
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- has_attributes :proxy_depositor, :on_behalf_of, datastream: :properties, multiple: false
7
+ property :proxy_depositor, predicate: ::RDF::URI.new('http://scholarsphere.psu.edu/ns#proxyDepositor'), multiple: false do |index|
8
+ index.as :symbol
9
+ end
10
+
11
+ # This value is set when a user indicates they are depositing this for someone else
12
+ property :on_behalf_of, predicate: ::RDF::URI.new('http://scholarsphere.psu.edu/ns#onBehalfOf'), multiple: false do |index|
13
+ index.as :symbol
14
+ end
15
+
8
16
  after_create :create_transfer_request
9
17
  end
10
18
 
19
+
11
20
  def create_transfer_request
12
- Sufia.queue.push(ContentDepositorChangeEventJob.new(pid, on_behalf_of)) if on_behalf_of.present?
21
+ Sufia.queue.push(ContentDepositorChangeEventJob.new(id, on_behalf_of)) if on_behalf_of.present?
13
22
  end
14
23
 
15
24
  def request_transfer_to(target)
16
25
  raise ArgumentError, "Must provide a target" unless target
17
26
  deposit_user = ::User.find_by_user_key(depositor)
18
- ProxyDepositRequest.create!(pid: pid, receiving_user: target, sending_user: deposit_user)
27
+ ProxyDepositRequest.create!(pid: id, receiving_user: target, sending_user: deposit_user)
19
28
  end
20
29
  end
21
30
  end
@@ -5,10 +5,7 @@ module Sufia
5
5
  version = content.latest_version
6
6
  # content datastream not (yet?) present
7
7
  return if version.nil?
8
- VersionCommitter.create(obj_id: version.pid,
9
- datastream_id: version.dsid,
10
- version_id: version.versionID,
11
- committer_login: user.user_key)
8
+ VersionCommitter.create(version_id: version.to_s, committer_login: user.user_key)
12
9
  end
13
10
 
14
11
  end
@@ -9,15 +9,22 @@ module Sufia
9
9
 
10
10
  def remove_blank_assertions
11
11
  terms_for_editing.each do |key|
12
- self[key] = nil if self[key] == ['']
12
+ if self[key] == ['']
13
+ self[key] = []
14
+ changed_attributes.delete(key) if attribute_was(key) == []
15
+ end
13
16
  end
14
17
  end
15
18
 
16
19
  # override this method if you need to initialize more complex RDF assertions (b-nodes)
17
20
  def initialize_fields
18
- terms_for_editing.each do |key|
21
+ terms_for_editing.select { |key| self[key].blank? }.each do |key|
19
22
  # if value is empty, we create an one element array to loop over for output
20
- self[key] = [''] if self[key].empty?
23
+ if self.class.multiple?(key)
24
+ self[key] = ['']
25
+ else
26
+ self[key] = ''
27
+ end
21
28
  end
22
29
  end
23
30
 
@@ -40,10 +47,10 @@ module Sufia
40
47
 
41
48
  def to_jq_upload
42
49
  return {
43
- "name" => self.title,
44
- "size" => self.file_size,
50
+ "name" => title,
51
+ "size" => file_size,
45
52
  "url" => "/files/#{noid}",
46
- "thumbnail_url" => self.pid,
53
+ "thumbnail_url" => id,
47
54
  "delete_url" => "deleteme", # generic_file_path(id: id),
48
55
  "delete_type" => "DELETE"
49
56
  }
@@ -8,23 +8,25 @@ module Sufia
8
8
 
9
9
  # OVERRIDE to support Hydra::Datastream::Properties which does not
10
10
  # respond to :depositor_values but :depositor
11
- # Adds metadata about the depositor to the asset
12
- # Most important behavior: if the asset has a rightsMetadata datastream, this method will add +depositor_id+ to its individual edit permissions.
13
-
11
+ # Adds metadata about the depositor to the asset and ads +depositor_id+ to
12
+ # its individual edit permissions.
14
13
  def apply_depositor_metadata(depositor)
15
- rights_ds = self.datastreams["rightsMetadata"]
16
- prop_ds = self.datastreams["properties"]
17
14
  depositor_id = depositor.respond_to?(:user_key) ? depositor.user_key : depositor
18
15
 
19
- rights_ds.update_indexed_attributes([:edit_access, :person]=>depositor_id) unless rights_ds.nil?
20
- prop_ds.depositor = depositor_id unless prop_ds.nil?
16
+ self.edit_users += [depositor_id]
17
+ self.depositor = depositor_id
21
18
 
22
19
  return true
23
20
  end
24
21
 
25
22
  def to_s
26
- return Array(title).join(" | ") if title.present?
27
- label || "No Title"
23
+ if title.present?
24
+ Array(title).join(" | ")
25
+ elsif label.present?
26
+ Array(label).join(" | ")
27
+ else
28
+ "No Title"
29
+ end
28
30
  end
29
31
 
30
32
  end
@@ -18,50 +18,33 @@ module Sufia::User
18
18
  # Users should be followable
19
19
  acts_as_followable
20
20
 
21
- # Set up proxy-related relationships
21
+ # Setup accessible (or protected) attributes for your model
22
22
  has_many :proxy_deposit_requests, foreign_key: 'receiving_user_id'
23
+
23
24
  has_many :deposit_rights_given, foreign_key: 'grantor_id', class_name: 'ProxyDepositRights', dependent: :destroy
24
25
  has_many :can_receive_deposits_from, through: :deposit_rights_given, source: :grantee
26
+
25
27
  has_many :deposit_rights_received, foreign_key: 'grantee_id', class_name: 'ProxyDepositRights', dependent: :destroy
26
28
  has_many :can_make_deposits_for, through: :deposit_rights_received, source: :grantor
27
29
 
28
- # Validate and normalize ORCIDs
29
- validates_with OrcidValidator
30
- after_validation :normalize_orcid
31
-
32
- # Set up user profile avatars
33
30
  mount_uploader :avatar, AvatarUploader, mount_on: :avatar_file_name
34
31
  validates_with AvatarValidator
35
-
36
32
  has_many :trophies
37
33
  attr_accessor :update_directory
38
34
  end
39
35
 
40
- # Coerce the ORCID into URL format
41
- def normalize_orcid
42
- # Skip normalization if:
43
- # 1. validation has already flagged the ORCID as invalid
44
- # 2. the orcid field is blank
45
- # 3. the orcid is already in its normalized form
46
- return if self.errors[:orcid].first.present? || self.orcid.blank? || self.orcid.starts_with?('http://orcid.org/')
47
- bare_orcid = /\d{4}-\d{4}-\d{4}-\d{4}/.match(self.orcid).string
48
- self.orcid = "http://orcid.org/#{bare_orcid}"
49
- end
50
-
51
36
  # Format the json for select2 which requires just an id and a field called text.
52
37
  # If we need an alternate format we should probably look at a json template gem
53
38
  def as_json(opts = nil)
54
- { id: user_key, text: display_name ? "#{display_name} (#{user_key})" : user_key }
39
+ {id: user_key, text: display_name ? "#{display_name} (#{user_key})" : user_key}
55
40
  end
56
41
 
57
42
  def email_address
58
- self.email
43
+ return self.email
59
44
  end
60
45
 
61
46
  def name
62
- self.display_name.titleize || raise
63
- rescue
64
- self.user_key
47
+ return self.display_name.titleize || self.user_key rescue self.user_key
65
48
  end
66
49
 
67
50
  # Redefine this for more intuitive keys in Redis
@@ -72,13 +55,13 @@ module Sufia::User
72
55
 
73
56
  def trophy_files
74
57
  trophies.map do |t|
75
- ::GenericFile.load_instance_from_solr(Sufia::Noid.namespaceize(t.generic_file_id))
58
+ ::GenericFile.load_instance_from_solr(t.generic_file_id)
76
59
  end
77
60
  end
78
61
 
79
62
  # method needed for messaging
80
63
  def mailboxer_email(obj=nil)
81
- nil
64
+ return nil
82
65
  end
83
66
 
84
67
  # The basic groups method, override or will fallback to Sufia::Ldap::User
@@ -102,9 +85,7 @@ module Sufia::User
102
85
  [:email, :login, :display_name, :address, :admin_area,
103
86
  :department, :title, :office, :chat_id, :website, :affiliation,
104
87
  :telephone, :avatar, :group_list, :groups_last_update, :facebook_handle,
105
- :twitter_handle, :googleplus_handle, :linkedin_handle, :remove_avatar,
106
- :orcid
107
- ]
88
+ :twitter_handle, :googleplus_handle, :linkedin_handle, :remove_avatar]
108
89
  end
109
90
 
110
91
  def current
@@ -138,5 +119,7 @@ module Sufia::User
138
119
  def from_url_component(component)
139
120
  User.find_by_user_key(component.gsub(/-dot-/, '.'))
140
121
  end
122
+
141
123
  end
124
+
142
125
  end
@@ -1,4 +1,4 @@
1
- class FileContentDatastream < ActiveFedora::Datastream
1
+ class FileContentDatastream < ActiveFedora::File
2
2
  include Hydra::Derivatives::ExtractMetadata
3
3
  include Sufia::FileContent::Versions
4
4
  end
@@ -1,7 +1,7 @@
1
1
  class FitsDatastream < ActiveFedora::OmDatastream
2
2
  include OM::XML::Document
3
3
 
4
- def prefix
4
+ def prefix(_)
5
5
  ""
6
6
  end
7
7
 
@@ -5,8 +5,8 @@ class FileDownloadStat < ActiveRecord::Base
5
5
  [ self.class.convert_date(date), downloads ]
6
6
  end
7
7
 
8
- def self.statistics file_id, start_date, user_id=nil
9
- combined_stats file_id, start_date, :downloads, :totalEvents, user_id
8
+ def self.statistics file_id, start_date
9
+ combined_stats file_id, start_date, :downloads, :totalEvents
10
10
  end
11
11
 
12
12
  # Sufia::Download is sent to Sufia::Analytics.profile as #sufia__download
@@ -3,17 +3,13 @@ class FileUsage
3
3
  attr_accessor :id, :created, :path, :downloads, :pageviews
4
4
 
5
5
  def initialize id
6
- file = ::GenericFile.find(id)
7
- user = User.where(email: file.depositor).first
8
- user_id = user ? user.id : nil
9
-
10
6
  self.id = id
11
7
  self.path = Sufia::Engine.routes.url_helpers.generic_file_path(Sufia::Noid.noidify(id))
12
8
  earliest = Sufia.config.analytic_start_date
13
- self.created = DateTime.parse(file.create_date)
9
+ self.created = ::GenericFile.find(id).create_date
14
10
  self.created = earliest > created ? earliest : created unless earliest.blank?
15
- self.downloads = FileDownloadStat.to_flots FileDownloadStat.statistics(id, created, user_id)
16
- self.pageviews = FileViewStat.to_flots FileViewStat.statistics(id, created, user_id)
11
+ self.downloads = FileDownloadStat.to_flots FileDownloadStat.statistics(id, created)
12
+ self.pageviews = FileViewStat.to_flots FileViewStat.statistics(id, created)
17
13
  end
18
14
 
19
15
  def total_downloads
@@ -23,8 +19,8 @@ class FileUsage
23
19
  def total_pageviews
24
20
  self.pageviews.reduce(0) { |total, result| total + result[1].to_i }
25
21
  end
26
-
27
- # Package data for visualization using JQuery Flot
22
+
23
+ # Package data for visualization using JQuery Flot
28
24
  def to_flot
29
25
  [
30
26
  { label: "Pageviews", data: pageviews },
@@ -5,8 +5,8 @@ class FileViewStat < ActiveRecord::Base
5
5
  [ self.class.convert_date(date), views ]
6
6
  end
7
7
 
8
- def self.statistics file_id, start_date, user_id=nil
9
- combined_stats file_id, start_date, :views, :pageviews, user_id
8
+ def self.statistics file_id, start_date
9
+ combined_stats file_id, start_date, :views, :pageviews
10
10
  end
11
11
 
12
12
  # Sufia::Download is sent to Sufia::Analytics.profile as #sufia__download