Class: Site

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Property, Property::Serialization::JSON, RubyLess, Zena::Use::MLIndex::SiteMethods
Defined in:
app/models/site.rb

Overview

A zena installation supports many sites. Each site is uniquely identified by it’s host name. The #Site model holds configuration information for a site:

host

Unique host name. (teti.ch, zenadmin.org, dev.example.org, …)

root_id

Site root node id. This is the only node in the site without a parent.

anon_id

Anonymous user id. This user is the ‘public’ user of the site. Even if authorize is set to true, this user is needed to configure the defaults for all newly created users.

public_group_id

Id of the ‘public’ group. Every user of the site (with ‘anonymous user’) belongs to this group.

site_group_id

Id of the ‘site’ group. Every user except anonymous are part of this group. This group can be seen as the ‘logged in users’ group.

name

Site name (used to display grouped information for cross sites users).

authorize

If this is set to true a login is required: anonymous visitor will not be allowed to browse the site as there is no login/password for the ‘anonymous user’.

languages

A comma separated list of the languages used for the current site. Do not insert spaces in this list.

default_lang

The default language of the site.

Constant Summary collapse

ACTIONS =
%w{clear_cache rebuild_index}
@@attributes_for_form =
{
  :bool => %w{authentication http_auth auto_publish},
  :text => %w{name languages default_lang},
}

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Zena::Use::MLIndex::SiteMethods

included

Class Method Details

.attributes_for_formObject

List of attributes that can be configured in the admin form



184
185
186
# File 'app/models/site.rb', line 184

def attributes_for_form
  @@attributes_for_form
end

.create_for_host(host, su_password, opts = {}) ⇒ Object

Create a new site in the database. This should not be called directly. Use rake zena:mksite HOST= instead

Raises:

  • (Exception)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/site.rb', line 48

def create_for_host(host, su_password, opts={})
  params = {
    :name                      => host.split('.').first,
    :authentication            => false,
    :auto_publish              => true,
    :redit_time                => '2h',
    :languages                 => '',
    :default_lang              => "en",
    :usr_prototype_attributes  => "{'klass' => 'Contact'}"
  }.merge(opts)
  langs = params[:languages].split(',').map(&:strip)
  langs += [params[:default_lang]] unless langs.include?(params[:default_lang])
  params[:languages] = langs.map{|l| l[0..1]}.join(',')
  params[:default_lang] = params[:default_lang][0..1]


  site      = self.new(params)
  site.host = host
  site.save
  site.instance_variable_set(:@being_created, true)

  if site.new_record?
    return site
  end

  # =========== CREATE zip counter ==========================
  connection.execute "INSERT INTO zips (site_id, zip) VALUES (#{site[:id]},0)"

  # =========== CREATE Admin User ===========================

  # create admin user
  admin_user = User.new_no_defaults(
    :login => 'admin',           :password => su_password,
    :lang  => site.default_lang, :status => User::Status[:admin])
  admin_user.site = site

  Thread.current[:visitor] = admin_user

  unless admin_user.save
    # rollback
    Site.connection.execute "DELETE FROM #{Site.table_name} WHERE id = #{site.id}"
    Site.connection.execute "DELETE FROM zips WHERE site_id = #{site.id}"
    raise Exception.new("Could not create admin user for site [#{host}] (site#{site[:id]})\n#{admin_user.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}")
  end

  # =========== CREATE PUBLIC, ADMIN, SITE GROUPS ===========
  # create public group
  pub = site.send(:secure,Group) { Group.create(:name => 'public') }
  raise Exception.new("Could not create public group for site [#{host}] (site#{site[:id]})\n#{pub.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") if pub.new_record?

  # create admin group
  editors = site.send(:secure,Group) { Group.create( :name => 'editors') }
  raise Exception.new("Could not create editors group for site [#{host}] (site#{site[:id]})\n#{editors.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") if editors.new_record?

  # add admin to the 'editors group'
  editors.users << admin_user

  # create site group
  sgroup = site.send(:secure,Group) { Group.create( :name => 'site') }
  raise Exception.new("Could not create site group for site [#{host}] (site#{site[:id]})\n#{sgroup.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") if sgroup.new_record?

  site.public_group_id = pub[:id]
  site.site_group_id   = sgroup[:id]
  site.groups << pub << sgroup << editors

  # Reload group_ids in admin
  admin_user.instance_variable_set(:@group_ids, nil)

  # =========== CREATE Anonymous User =====================
  # create anon user

  anon = site.send(:secure, User) do
    User.new_no_defaults( :login => nil, :password => nil,
    :lang => site.default_lang, :status => User::Status[:moderated])
  end

  anon.site = site
  raise Exception.new("Could not create anonymous user for site [#{host}] (site#{site[:id]})\n#{anon.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") unless anon.save
  site[:anon_id] = anon[:id]

  # =========== CREATE ROOT NODE ============================

  root = site.send(:secure,Project) do
    Project.create( :title => site.name, :rgroup_id => pub[:id], :wgroup_id => sgroup[:id], :dgroup_id => editors[:id], :title => site.name, :v_status => Zena::Status::Pub)
  end

  raise Exception.new("Could not create root node for site [#{host}] (site#{site[:id]})\n#{root.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") if root.new_record?

  Node.connection.execute "UPDATE nodes SET section_id = id, project_id = id WHERE id = '#{root[:id]}'"

  raise Exception.new("Could not publish root node for site [#{host}] (site#{site[:id]})\n#{root.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") unless (root.v_status == Zena::Status::Pub || root.publish)

  site.root_id = root[:id]
  # Make sure safe definitions on Time/Array/String are available on prop_eval validation.
  Zena::Use::ZafuSafeDefinitions
  # Should not be needed since we load PropEval in Node, but it does not work
  # without when doing 'mksite' (works in tests).
  Node.safe_method :now => {:class => Time, :method => 'Time.now'}

  # =========== UPDATE SITE =================================
  # save site definition
  raise Exception.new("Could not save site definition for site [#{host}] (site#{site[:id]})\n#{site.errors.map{|k,v| "[#{k}] #{v}"}.join("\n")}") unless site.save

  # =========== LOAD INITIAL DATA (default skin + roles) =============

  nodes = site.send(:secure, Node) { Node.create_nodes_from_folder(:folder => File.join(Zena::ROOT, 'db', 'init', 'base'), :parent_id => root[:id], :defaults => { :v_status => Zena::Status::Pub, :rgroup_id => pub[:id], :wgroup_id => sgroup[:id], :dgroup_id => editors[:id] } ) }.values
  # == set skin id to 'default' for all elements in the site == #
  skin = nodes.detect {|n| n.kind_of?(Skin) }
  Node.connection.execute "UPDATE nodes SET skin_id = '#{skin.id}' WHERE site_id = '#{site[:id]}'"

  # =========== CREATE CONTACT PAGES ==============
  {
    admin_user => {'first_name' => 'Admin',     'last_name' => 'User'},
    anon       => {'first_name' => 'Anonymous', 'last_name' => 'User'}
  }.each do |user, attrs|
    # forces @node creation
    user.node_attributes = attrs
    user.send(:create_node)
    user.save!
  end

  # == done.
  Site.logger.info "=========================================================="
  Site.logger.info "  NEW SITE CREATED FOR [#{host}] (site#{site[:id]})"
  Site.logger.info "=========================================================="

  site.instance_variable_set(:@being_created, false)
  site
end

.find_by_host(host) ⇒ Object



178
179
180
181
# File 'app/models/site.rb', line 178

def find_by_host(host)
  host = $1 if host =~ /^(.*)\.$/
  self.find(:first, :conditions => ['host = ?', host]) rescue nil
end

Instance Method Details

#admin_user_idsObject

Return the ids of the administrators of the current site.



251
252
253
254
# File 'app/models/site.rb', line 251

def admin_user_ids
  # TODO: PERFORMANCE admin_user_ids could be cached in the 'site' record.
  @admin_user_ids ||= secure!(User) { User.find(:all, :conditions => "status >= #{User::Status[:admin]}") }.map {|r| r[:id]}
end

#anonObject

Return the anonymous user, the one used by anonymous visitors to visit the public part of the site.



214
215
216
# File 'app/models/site.rb', line 214

def anon
  @anon ||= User.find_by_id_and_site_id(self[:anon_id], self.id)
end

#any_adminObject

Return an admin user, this user is used to rebuild index/vhash/etc.



219
220
221
# File 'app/models/site.rb', line 219

def any_admin
  @any_admin ||= User.find_by_status_and_site_id(User::Status[:admin], self.id)
end

#api_groupObject

Return the API group: the one in which API visitors must be to use the API.



241
242
243
# File 'app/models/site.rb', line 241

def api_group
  @api_group ||= secure(Group) { Group.find_by_id(self[:api_group_id]) }
end

#authentication?Boolean

Return true if the site is configured to force authentication

Returns:

  • (Boolean)


257
258
259
# File 'app/models/site.rb', line 257

def authentication?
  self[:authentication]
end

#auto_publish?Boolean

Return true if the site is configured to automatically publish redactions

Returns:

  • (Boolean)


262
263
264
# File 'app/models/site.rb', line 262

def auto_publish?
  self[:auto_publish]
end

#being_created?Boolean

Returns:

  • (Boolean)


296
297
298
# File 'app/models/site.rb', line 296

def being_created?
  @being_created
end

#clear_cache(clear_zafu = true) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'app/models/site.rb', line 340

def clear_cache(clear_zafu = true)
  path = "#{SITES_ROOT}#{self.public_path}"
  Site.logger.error("\n-----------------\nCLEAR CACHE FOR SITE #{host}\n-----------------\n")

  if File.exist?(path)
    Dir.foreach(path) do |elem|
      next unless elem =~ /^(\w\w\.html|\w\w|login\.html)$/
      FileUtils.rmtree(File.join(path, elem))
    end

    Site.connection.execute "DELETE FROM caches WHERE site_id = #{self[:id]}"
    Site.connection.execute "DELETE FROM cached_pages_nodes WHERE cached_pages_nodes.node_id IN (SELECT nodes.id FROM nodes WHERE nodes.site_id = #{self[:id]})"
    Site.connection.execute "DELETE FROM cached_pages WHERE site_id = #{self[:id]}"
  end

  if clear_zafu
    path = "#{SITES_ROOT}#{self.zafu_path}"
    if File.exist?(path)
      FileUtils.rmtree(path)
    end
  end

  true
end

#data_pathObject

Return path for documents data: RAILS_ROOT/sites/host/data You can symlink the ‘data’ directory if you need to keep the data in some other place.



203
204
205
# File 'app/models/site.rb', line 203

def data_path
  "/#{self[:host]}/data"
end

#iformatsObject



304
305
306
307
308
309
310
311
312
313
# File 'app/models/site.rb', line 304

def iformats
 @iformats ||= begin
   $iformats ||= {} # mem cache
   site_formats = $iformats[self[:id]]
   if !site_formats || self[:formats_updated_at] != site_formats[:updated_at]
     site_formats = $iformats[self[:id]] = Iformat.formats_for_site(self[:id]) # reload
   end
   site_formats
  end
end

#iformats_updated!Object



315
316
317
318
319
320
# File 'app/models/site.rb', line 315

def iformats_updated!
  Site.connection.execute "UPDATE sites SET formats_updated_at = (SELECT updated_at FROM iformats WHERE site_id = #{self[:id]} ORDER BY iformats.updated_at DESC LIMIT 1) WHERE id = #{self[:id]}"
  if $iformats
    $iformats[self[:id]] = @iformats = nil
  end
end

#is_admin?(user) ⇒ Boolean

Return true if the given user is an administrator for this site.

Returns:

  • (Boolean)


246
247
248
# File 'app/models/site.rb', line 246

def is_admin?(user)
  admin_user_ids.include?(user[:id])
end

#lang_listObject

Return an array with the languages for the site.



292
293
294
# File 'app/models/site.rb', line 292

def lang_list
  (self[:languages] || "").split(',').map(&:strip)
end

#languages=(s) ⇒ Object



300
301
302
# File 'app/models/site.rb', line 300

def languages=(s)
  self[:languages] = s.split(',').map(&:strip).join(',')
end

#protected_group_idsObject

ids of the groups that cannot be removed



282
283
284
# File 'app/models/site.rb', line 282

def protected_group_ids
  [site_group_id, public_group_id]
end

#protected_user_idsObject

ids of the users that cannot be removed



287
288
289
# File 'app/models/site.rb', line 287

def protected_user_ids
  [anon_id, visitor.id] # cannot remove self
end

#public_groupObject

Return the public group: the one in which every visitor belongs.



231
232
233
# File 'app/models/site.rb', line 231

def public_group
  @public_group ||= secure(Group) { Group.find(self[:public_group_id]) }
end

#public_pathObject

Return path for static/cached content served by proxy: RAILS_ROOT/sites/host/public If you need to serve from another directory, we do not store the path into the sites table for security reasons. The easiest way around this limitation is to symlink the ‘public’ directory.



197
198
199
# File 'app/models/site.rb', line 197

def public_path
  "/#{self[:host]}/public"
end

#rebuild_fullpath(parent_id = nil, parent_fullpath = "", parent_basepath = "", start = []) ⇒ Object

Recreates the fullpath (‘/zip/zip/zip’). TODO: find a way to use SiteWorker (need to remove get_nodes): fix rake when this is done.



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'app/models/site.rb', line 384

def rebuild_fullpath(parent_id = nil, parent_fullpath = "", parent_basepath = "", start=[])
  raise Zena::InvalidRecord, "Infinit loop in 'ancestors' (#{start.inspect} --> #{parent_id})" if start.include?(parent_id)
  start += [parent_id]
  i = 0
  batch_size = 100
  children = []
  while true
    rec = Zena::Db.fetch_attributes(['id', 'fullpath', 'basepath', 'custom_base', 'zip'], 'nodes', "parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"} AND site_id = #{self.id} ORDER BY id ASC LIMIT #{batch_size} OFFSET #{i * batch_size}")
    break if rec.empty?
    rec.each do |rec|
      if parent_id
        rec['fullpath'] = parent_fullpath == '' ? rec['zip'] : "#{parent_fullpath}/#{rec['zip']}"
      else
        # root node
        rec['fullpath'] = ''
      end

      if rec['custom_base'] == Zena::Db::TRUE_RESULT
        rec['basepath'] = rec['fullpath']
      else
        rec['basepath'] = parent_basepath
      end

      id = rec.delete('id')
      children << [id, rec['fullpath'], rec['basepath'], start]
      Zena::Db.execute "UPDATE nodes SET #{rec.map {|k,v| "#{Zena::Db.connection.quote_column_name(k)}=#{Zena::Db.quote(v)}"}.join(', ')} WHERE id = #{id}"
    end
    # 50 more
    i += 1
  end
  children.each do |child|
    rebuild_fullpath(*child)
  end

  true
end

#rebuild_index(nodes = nil, page = nil, page_count = nil) ⇒ Object

Rebuild property indices for the Site. This method uses the Worker thread to rebuild and works on chunks of 50 nodes.

The visitor used during index rebuild should be an admin user (to index unpublished templates).



426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'app/models/site.rb', line 426

def rebuild_index(nodes = nil, page = nil, page_count = nil)
  if !page
    Site.logger.error("\n----------------- REBUILD INDEX FOR SITE #{host} -----------------\n")
    Zena::SiteWorker.perform(self, :rebuild_index)
  else
    # do things
    nodes.each do |node|
      node.rebuild_index!
    end
  end

  true
end

#rebuild_vhash(nodes = nil, page = nil, page_count = nil) ⇒ Object

Rebuild vhash indices for the Site. This method uses the Worker thread to rebuild and works on chunks of 50 nodes.



367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'app/models/site.rb', line 367

def rebuild_vhash(nodes = nil, page = nil, page_count = nil)
  if !nodes
    Site.logger.error("\n----------------- REBUILD VHASH FOR SITE #{host} -----------------\n")
    Zena::SiteWorker.perform(self, :rebuild_vhash)
  else
    # do things
    nodes.each do |node|
      node.rebuild_vhash
      Node.connection.execute "UPDATE nodes SET publish_from = #{Node.connection.quote(node.publish_from)}, vhash = #{Node.connection.quote(node.vhash.to_json)} WHERE id = #{node.id}"
    end
  end

  true
end

#redit_timeObject

Return the time between version updates below which no new version is created. This returns a string of the form “3 hours 45 minutes”



277
278
279
# File 'app/models/site.rb', line 277

def redit_time
  (self[:redit_time] || 0).as_duration
end

#redit_time=(val) ⇒ Object

Set redit time from a string of the form “1d 4h 5s” or “4 days”



267
268
269
270
271
272
273
# File 'app/models/site.rb', line 267

def redit_time=(val)
  if val.kind_of?(String)
    self[:redit_time] = val.to_duration
  else
    self[:redit_time] = val
  end
end

#root_nodeObject

Return the root node or a dummy if the visitor cannot view root node (such as during a 404 or login rendering).



225
226
227
228
# File 'app/models/site.rb', line 225

def root_node
  @root ||= secure(Node) { Node.find(self.root_id) } ||
            Node.new(:title => host)
end

#site_groupObject

Return the site group: the one in which every visitor except ‘anonymous’ belongs (= all logged in users).



236
237
238
# File 'app/models/site.rb', line 236

def site_group
  @site_group ||= secure(Group) { Group.find(self[:site_group_id]) }
end

#virtual_classesObject



322
323
324
325
326
327
328
329
330
331
# File 'app/models/site.rb', line 322

def virtual_classes
 @iformats ||= begin
   $iformats ||= {} # mem cache
   site_formats = $iformats[self[:id]]
   if !site_formats || self[:formats_updated_at] != site_formats[:updated_at]
     site_formats = $iformats[self[:id]] = Iformat.formats_for_site(self[:id]) # reload
   end
   site_formats
  end
end

#zafu_pathObject

Return the path for zafu rendered templates: RAILS_ROOT/sites/host/zafu



208
209
210
# File 'app/models/site.rb', line 208

def zafu_path
  "/#{self[:host]}/zafu"
end