Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merges the 'dist' branch

git-svn-id: svn://cherokee-project.com/cherokee/trunk@6490 5dc97367-97f1-0310-9951-d761b3857238
  • Loading branch information...
commit 9962037e9dc956d4e04bbcc7be690febc61630e2 1 parent 6290e1b
Alvaro Lopez Ortega alobbs authored
8 admin/Page.py
View
@@ -153,7 +153,7 @@ def __init__ (self, title, headers=[], body_id=None, **kwargs):
template['save'] = _('Save')
template['home'] = _('Home')
template['status'] = _('Status')
- template['market'] = _('Market')
+ template['market'] = _('Apps')
template['general'] = _('General')
template['vservers'] = _('vServers')
template['sources'] = _('Sources')
@@ -167,12 +167,6 @@ def __init__ (self, title, headers=[], body_id=None, **kwargs):
if body_id:
template['body_props'] = ' id="body-%s"' %(body_id)
- # Hide/Show Market icon
- if int (CTK.cfg.get_val("admin!ows!enabled", OWS_ENABLE)):
- template['market_menu_entry'] = '<li id="nav-market"><a href="/market">%(market)s</a></li>'
- else:
- template['market_menu_entry'] = ''
-
# Save dialog
dialog = CTK.DialogProxyLazy (URL_SAVE, {'title': _(SAVED_NOTICE), 'autoOpen': False, 'draggable': False, 'width': 500})
CTK.publish (URL_SAVE, Save, dialog=dialog)
167 admin/market/Distro.py
View
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#
+# Cherokee-admin
+#
+# Authors:
+# Alvaro Lopez Ortega <alvaro@alobbs.com>
+#
+# Copyright (C) 2001-2011 Alvaro Lopez Ortega
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of version 2 of the GNU General Public
+# License as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+
+import CTK
+
+import re
+import os
+import sys
+import gzip
+import time
+import urllib2
+import threading
+
+from util import *
+from consts import *
+from ows_consts import *
+from configured import *
+
+
+global_index = None
+global_index_lock = threading.Lock()
+
+def Index():
+ global global_index
+
+ global_index_lock.acquire()
+
+ if not global_index:
+ try:
+ global_index = Index_Class()
+ except:
+ global_index_lock.release()
+ raise
+
+ global_index_lock.release()
+ return global_index
+
+
+def cached_download (url, return_content=False):
+ # Open the connection
+ request = urllib2.Request (url)
+ opener = urllib2.build_opener()
+
+ # Cache file
+ url_md5 = CTK.util.md5 (url).hexdigest()
+ cache_file = os.path.join (CHEROKEE_OWS_DIR, url_md5)
+
+ tmp = url.split('?',1)[0] # Remove params
+ ext = tmp.split('.')[-1] # Extension
+ if len(ext) in range(2,5):
+ cache_file += '.%s'%(ext)
+
+ # Check previos version
+ if os.path.exists (cache_file):
+ s = os.stat (cache_file)
+ t = time.strftime ("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(s.st_mtime))
+ request.add_header ('If-Modified-Since', t)
+
+ # Send request
+ try:
+ if sys.version_info < (2, 6):
+ stream = opener.open (request)
+ else:
+ stream = opener.open (request, timeout=10)
+ except urllib2.HTTPError, e:
+ if e.code == 304:
+ # 304: Not Modified
+ if return_content:
+ return open (cache_file, 'rb').read()
+ return cache_file
+ raise
+
+ # Content
+ content = stream.read()
+
+ # Store
+ f = open (cache_file, "wb+")
+ f.write (content)
+ f.close()
+
+ if return_content:
+ return open (cache_file, 'rb').read()
+ return cache_file
+
+
+class Index_Class:
+ def __init__ (self):
+ repo_url = CTK.cfg.get_val ('admin!ows!repository', REPO_MAIN)
+
+ self.url = os.path.join (repo_url, 'index.py.gz')
+ self.content = {}
+ self.local_file = None
+ self.error = False
+
+ # Initialization
+ self.Update()
+
+ def Update (self):
+ # Download
+ try:
+ local_file = cached_download (self.url)
+ except urllib2.HTTPError, e:
+ self.error = True
+ return
+
+ # Shortcut: Do not parse if it hasn't changed
+ if ((local_file == self.local_file) and
+ (os.stat(local_file).st_mtime == os.stat(self.local_file).st_mtime)):
+ return
+ self.local_file = local_file
+
+ # Read
+ f = gzip.open (local_file, 'rb')
+ content = f.read()
+ f.close()
+
+ # Parse
+ self.content = CTK.util.data_eval (content)
+
+ def __getitem__ (self, k):
+ return self.content[k]
+
+ def get (self, k, default=None):
+ return self.content.get (k, default)
+
+ # Helpers
+ #
+ def get_package (self, pkg, prop=None):
+ pkg = self.content.get('packages',{}).get(pkg)
+ if pkg and prop:
+ return pkg.get(prop)
+ return pkg
+
+ def get_package_list (self):
+ return self.content.get('packages',{}).keys()
+
+
+if __name__ == "__main__":
+ i = Index()
+ i.Download()
+ i.Parse()
+
+ print i['build_date']
+ print i.get_package_list()
+ print i.get_package('phpbb')
+ print i.get_package('phpbb', 'installation')
151 admin/market/Install.py
View
@@ -40,6 +40,7 @@
import SystemStats
import SaveButton
import Cherokee
+import Distro
import popen
import CommandProgress
import InstallUtil
@@ -66,6 +67,10 @@
NO_ROOT_P1 = N_("Since the installations may require administrator to execute system administration commands, it is required to run Cherokee-admin under a user with administrator privileges.")
NO_ROOT_P2 = N_("Please, run cherokee-admin as root to solve the problem.")
+MIN_VER_H1 = N_("The package cannot be installed")
+MIN_VER_p1 = N_("This package required at least Cherokee %(version)s to be installed.")
+
+
URL_INSTALL_WELCOME = "%s/install/welcome" %(URL_MAIN)
URL_INSTALL_INIT_CHECK = "%s/install/check" %(URL_MAIN)
URL_INSTALL_PAY_CHECK = "%s/install/pay" %(URL_MAIN)
@@ -81,14 +86,18 @@
class InstallDialog (CTK.Dialog):
- def __init__ (self, info):
- title = "%s%s" %(info['application_name'], _("Cherokee Market"))
+ def __init__ (self, app_name):
+ index = Distro.Index()
+ app = index.get_package (app_name, 'software')
+
+ title = "%s%s" %(app['name'], _("Cherokee Market"))
CTK.Dialog.__init__ (self, {'title': title, 'width': 600, 'minHeight': 300})
- self.info = info
+ self.info = app
- for key in ('application_id', 'application_name', 'currency_symbol', 'amount', 'currency'):
- CTK.cfg['tmp!market!install!app!%s'%(key)] = str(info[key])
+ # Copy a couple of preliminary config entries
+ CTK.cfg['tmp!market!install!app!application_id'] = app['id']
+ CTK.cfg['tmp!market!install!app!application_name'] = app['name']
self.refresh = CTK.RefreshableURL()
self.druid = CTK.Druid(self.refresh)
@@ -123,7 +132,7 @@ def __call__ (self):
class Welcome (Install_Stage):
def __safe_call__ (self):
# Ensure the current UID has enough priviledges
- if not InstallUtil.current_UID_is_admin():
+ if not InstallUtil.current_UID_is_admin() and 0:
box = CTK.Box()
box += CTK.RawHTML ('<h2>%s</h2>' %(_(NO_ROOT_H1)))
box += CTK.RawHTML ('<p>%s</p>' %(_(NO_ROOT_P1)))
@@ -135,6 +144,25 @@ def __safe_call__ (self):
return box.Render().toStr()
+ # Check the rest of pre-requisites
+ index = Distro.Index()
+ app_id = CTK.cfg.get_val('tmp!market!install!app!application_id')
+ app = index.get_package (app_id, 'software')
+
+ inst = index.get_package (app_id, 'installation')
+ if 'cherokee_min_ver' in inst:
+ version = inst['cherokee_min_ver']
+ if version_cmp (VERSION, version) < 0:
+ box = CTK.Box()
+ box += CTK.RawHTML ('<h2>%s</h2>' %(_(MIN_VER_H1)))
+ box += CTK.RawHTML ('<p>%s</p>' %(_(MIN_VER_P1)%(locals())))
+
+ buttons = CTK.DruidButtonsPanel()
+ buttons += CTK.DruidButton_Close(_('Cancel'))
+
+ box += buttons
+ return box.Render().toStr()
+
# Init the log file
Install_Log.reset()
Install_Log.log (".---------------------------------------------.")
@@ -151,112 +179,23 @@ def __safe_call__ (self):
# Render a welcome message
box = CTK.Box()
- box += CTK.RawHTML ('<h2>%s</h2>' %(_('Connecting to Cherokee Market')))
- box += CTK.RawHTML ('<h1>%s</h1>' %(_('Retrieving package information…')))
- box += CTK.RawHTML (js = CTK.DruidContent__JS_to_goto (box.id, URL_INSTALL_INIT_CHECK))
-
- # Dialog buttons
- buttons = CTK.DruidButtonsPanel()
- buttons += CTK.DruidButton_Close(_('Cancel'))
- box += buttons
+ box += CTK.RawHTML (js = CTK.DruidContent__JS_to_goto (box.id, URL_INSTALL_DOWNLOAD))
return box.Render().toStr()
-class Init_Check (Install_Stage):
- def __safe_call__ (self):
- app_id = CTK.cfg.get_val('tmp!market!install!app!application_id')
- app_name = CTK.cfg.get_val('tmp!market!install!app!application_name')
-
- info = {'cherokee_version': VERSION,
- 'system': SystemInfo.get_info()}
-
- cont = CTK.Box()
- xmlrpc = XmlRpcServer (OWS_APPS_INSTALL, user=OWS_Login.login_user, password=OWS_Login.login_password)
- install_info = xmlrpc.get_install_info (app_id, info)
-
- if install_info.get('error'):
- title = install_info['error']['error_title']
- errors = install_info['error']['error_strings']
-
- cont += CTK.RawHTML ("<h2>%s</h2>"%(_(title)))
- for error in errors:
- cont += CTK.RawHTML ("<p>%s</p>"%(_(error)))
-
- buttons = CTK.DruidButtonsPanel()
- buttons += CTK.DruidButton_Close(_('Close'))
- cont += buttons
-
- elif install_info['installable']:
- # Do not change this log line. It is used by the
- # Maintenance.py file to figure out the app name
- Install_Log.log ("Checking: %s, ID: %s = Installable, URL=%s" %(app_name, app_id, install_info['url']))
-
- CTK.cfg['tmp!market!install!download'] = install_info['url']
- cont += CTK.RawHTML (js = CTK.DruidContent__JS_to_goto (cont.id, URL_INSTALL_DOWNLOAD))
-
- else:
- Install_Log.log ("Checking: %s, ID: %s = Must check out first" %(app_name, app_id))
-
- cont += CTK.RawHTML ("<h2>%s %s</h2>"%(_('Checking out'), app_name))
- cont += CTK.RawHTML ("<p>%s</p>" %(_(NOTE_ALL_READY_TO_BUY_1)))
- cont += CTK.RawHTML ("<p>%s</p>" %(_(NOTE_ALL_READY_TO_BUY_2)))
-
- checkout = CTK.Button (_("Check Out"))
- checkout.bind ('click', CTK.DruidContent__JS_to_goto (cont.id, URL_INSTALL_PAY_CHECK) +
- CTK.JS.OpenWindow('%s/order/%s' %(OWS_STATIC, app_id)))
-
- buttons = CTK.DruidButtonsPanel()
- buttons += CTK.DruidButton_Close(_('Cancel'))
- buttons += checkout
- cont += buttons
-
- return cont.Render().toStr()
-
-
-class Pay_Check (Install_Stage):
+class Download (Install_Stage):
def __safe_call__ (self):
app_id = CTK.cfg.get_val('tmp!market!install!app!application_id')
app_name = CTK.cfg.get_val('tmp!market!install!app!application_name')
- info = {'cherokee_version': VERSION,
- 'system': SystemInfo.get_info()}
-
- xmlrpc = XmlRpcServer (OWS_APPS_INSTALL, user=OWS_Login.login_user, password=OWS_Login.login_password)
- install_info = xmlrpc.get_install_info (app_id, info)
-
- Install_Log.log ("Waiting for the payment acknowledge..")
+ # URL Package
+ index = Distro.Index()
+ pkg = index.get_package (app_id, 'package')
- box = CTK.Box()
- if install_info.get('due_payment'):
- set_timeout_js = "setTimeout (reload_druid, %s);" %(PAYMENT_CHECK_TIMEOUT)
- box += CTK.RawHTML ("<h2>%s %s</h2>"%(_('Checking out'), app_name))
- box += CTK.RawHTML ('<h1>%s</h1>' %(_("Waiting for the payment acknowledge…")))
- box += CTK.RawHTML (js="function reload_druid() {%s %s}" %(CTK.DruidContent__JS_to_goto (box.id, URL_INSTALL_PAY_CHECK), set_timeout_js))
- box += CTK.RawHTML (js=set_timeout_js)
-
- buttons = CTK.DruidButtonsPanel()
- buttons += CTK.DruidButton_Close(_('Cancel'))
- box += buttons
-
- else:
- Install_Log.log ("Payment ACK!")
-
- # Invalidate 'My Library' cache
- Library.Invalidate_Cache()
-
- # Move on
- CTK.cfg['tmp!market!install!download'] = install_info['url']
- box += CTK.RawHTML (js=CTK.DruidContent__JS_to_goto (box.id, URL_INSTALL_DOWNLOAD))
-
- return box.Render().toStr()
-
-
-class Download (Install_Stage):
- def __safe_call__ (self):
- app_id = CTK.cfg.get_val('tmp!market!install!app!application_id')
- app_name = CTK.cfg.get_val('tmp!market!install!app!application_name')
- url_download = CTK.cfg.get_val('tmp!market!install!download')
+ repo_url = CTK.cfg.get_val ('admin!ows!repository', REPO_MAIN)
+ url_download = os.path.join (repo_url, app_id, pkg['filename'])
+ CTK.cfg['tmp!market!install!download'] = url_download
# Local storage shortcut
pkg_filename_full = url_download.split('/')[-1]
@@ -271,7 +210,7 @@ def __safe_call__ (self):
pkg_revision = max (pkg_revision, int(tmp[0]))
if pkg_revision > 0:
- pkg_fullpath = os.path.join (CHEROKEE_OWS_DIR, "packages", app_id, '%s_%d.pkg' %(pkg_filename, pkg_revision))
+ pkg_fullpath = os.path.join (CHEROKEE_OWS_DIR, "packages", app_id, '%s_%d.cpk' %(pkg_filename, pkg_revision))
CTK.cfg['tmp!market!install!local_package'] = pkg_fullpath
Install_Log.log ("Using local repository package: %s" %(pkg_fullpath))
@@ -580,8 +519,8 @@ def __safe_call__ (self):
CTK.publish ('^%s$'%(URL_INSTALL_WELCOME), Welcome)
-CTK.publish ('^%s$'%(URL_INSTALL_INIT_CHECK), Init_Check)
-CTK.publish ('^%s$'%(URL_INSTALL_PAY_CHECK), Pay_Check)
+#CTK.publish ('^%s$'%(URL_INSTALL_INIT_CHECK), Init_Check)
+#CTK.publish ('^%s$'%(URL_INSTALL_PAY_CHECK), Pay_Check)
CTK.publish ('^%s$'%(URL_INSTALL_DOWNLOAD), Download)
CTK.publish ('^%s$'%(URL_INSTALL_SETUP), Setup)
CTK.publish ('^%s$'%(URL_INSTALL_POST_UNPACK), Post_unpack)
3  admin/market/Makefile.am
View
@@ -16,7 +16,8 @@ Library.py \
Util.py \
Maintenance.py \
ows_consts.py \
-CommandProgress.py
+CommandProgress.py \
+Distro.py
marketpydir = "$(datadir)/cherokee/admin/market"
marketpy_DATA = $(code)
264 admin/market/PageApp.py
View
@@ -25,6 +25,7 @@
import re
import CTK
+import Distro
import OWS_Login
from Util import *
@@ -55,29 +56,47 @@ def __init__ (self, name, supported):
self += CTK.RawHTML (name)
class Support_Block (CTK.Container):
+ REPLACEMENTS = {
+ 'macosx': 'MacOS X',
+ 'sqlite3': 'SQLite',
+ }
def __init__ (self, info, sure_inclusion):
CTK.Container.__init__ (self)
+ info = list(info)
+ info_lower = [x.lower() for x in info]
+ sure_lower = [x.lower() for x in sure_inclusion]
+
+ # Initial replacements
+ for rep in self.REPLACEMENTS:
+ if rep in info_lower:
+ info[info.index(rep)] = self.REPLACEMENTS[rep]
+ info_lower[info_lower.index(rep)] = self.REPLACEMENTS[rep].lower()
+
# These entries will always be shown
for entry in sure_inclusion:
- self += SupportBox.Support_Entry (entry, info.get(entry, False))
+ self += SupportBox.Support_Entry (entry, entry.lower() in info_lower)
# Additional entries
- rest = filter (lambda x: x not in (sure_inclusion), info.keys())
- for entry in rest:
- if info[entry]:
+ for entry in info:
+ if entry.lower() in sure_lower:
+ continue
+ if entry.lower() in info_lower:
self += SupportBox.Support_Entry (entry, True)
- def __init__ (self, info):
+ def __init__ (self, app_name):
CTK.Box.__init__ (self, {'class': 'market-support-box'})
- OSes = info['os']
- DBes = info['db']
- targets = info['target']
+ index = Distro.Index()
+ inst = index.get_package (app_name, 'installation')
+
+ OSes = inst['OS']
+ DBes = inst['DB']
+ modes = inst['modes']
self += CTK.RawHTML ('<h3>%s</h3>' %(_("Deployment methods")))
- self += SupportBox.Support_Entry (_('Virtual Server'), targets.get('host', False))
- self += SupportBox.Support_Entry (_('Directory'), targets.get('dir', False))
+ self += SupportBox.Support_Entry (_('Virtual Server'), 'vserver' in modes)
+ self += SupportBox.Support_Entry (_('Directory'), 'dir' in modes)
self += CTK.RawHTML ('<h3>%s</h3>' %(_("Operating Systems")))
self += SupportBox.Support_Block (OSes, ('Linux', 'MacOS X', 'Solaris', 'FreeBSD'))
@@ -86,220 +105,113 @@ def __init__ (self, info):
self += SupportBox.Support_Block (DBes, ('MySQL', 'PostgreSQL', 'SQLite', 'Oracle'))
-class App:
- cache_app = {}
- def format_func_app (self, info):
- cont = CTK.Container()
+class AppInfo (CTK.Box):
+ def __init__ (self, app_name):
+ CTK.Box.__init__ (self, {'class': 'cherokee-market-app'})
- self.menu += "%s %s" %(_('Application'), info['application_name'])
- cont += self.menu
+ index = Distro.Index()
- app_id = info['application_id']
- currency_symbol = CTK.util.to_utf8 (info['currency_symbol'])
+ app = index.get_package (app_name, 'software')
+ maintainer = index.get_package (app_name, 'maintainer')
# Install dialog
- install = InstallDialog (info)
- cont += install
+ install = InstallDialog (app_name)
- # Publisher / Author
+ # Author
by = CTK.Container()
by += CTK.RawHTML ('%s '%(_('By')))
- if info.get('author_url') and info.get('author_name'):
- by += CTK.LinkWindow (info['author_url'], CTK.RawHTML(info['author_name']))
- else:
- by += CTK.LinkWindow (info['publisher_url'], CTK.RawHTML(info['publisher_name']))
-
- # Buy / Install button
- if OWS_Login.is_logged():
- if Library.is_appID_in_library (app_id):
- buy = CTK.Button (_("Install"))
- else:
- if info['amount']:
- buy = CTK.Button ("%s%s %s" %(currency_symbol, info['amount'], _("Buy")))
- else:
- buy = CTK.Button ("%s, %s" %(_("Free"), _("Install")))
- buy.bind ('click', install.JS_to_show())
-
- else:
- link = CTK.Link ('#', CTK.RawHTML (_("Sign in")))
- link.bind ('click', self.login_dialog.JS_to_show())
+ by += CTK.LinkWindow (app['URL'], CTK.RawHTML(app['author']))
- login_txt = CTK.Box()
- login_txt += link
+ install_button = CTK.Button (_("Install"))
+ install_button.bind ('click', install.JS_to_show())
- if info['amount']:
- buy_button = CTK.Button ("%s%s %s" %(currency_symbol, info['amount'], _("Buy")), {"disabled": True})
- login_txt += CTK.RawHTML (" %s" %(_('to buy')))
- else:
- buy_button = CTK.Button ("%s, %s" %(_("Free"), _("Install")), {"disabled": True})
- login_txt += CTK.RawHTML (" %s" %(_('to install')))
+ # Report button
+ druid = CTK.Druid (CTK.RefreshableURL())
+ report_dialog = CTK.Dialog ({'title':(_("Report Application")), 'width': 480})
+ report_dialog += druid
+ druid.bind ('druid_exiting', report_dialog.JS_to_close())
- buy = CTK.Container()
- buy += buy_button
- buy += login_txt
+ report_link = CTK.Link (None, CTK.RawHTML(_("Report issue")))
+ report_link.bind ('click', report_dialog.JS_to_show() + \
+ druid.JS_to_goto('"%s/%s"'%(URL_REPORT, app_name)))
- # Report button
report = CTK.Container()
- if OWS_Login.is_logged():
- CTK.cfg['tmp!market!report!app_id'] = str(self.text_app_id)
-
- druid = CTK.Druid (CTK.RefreshableURL())
- report_dialog = CTK.Dialog ({'title':(_("Report Application")), 'width': 480})
- report_dialog += druid
- druid.bind ('druid_exiting', report_dialog.JS_to_close())
-
- report_link = CTK.Link (None, CTK.RawHTML(_("Report issue")))
- report_link.bind ('click', report_dialog.JS_to_show() + \
- druid.JS_to_goto('"%s"'%(URL_REPORT)))
-
- report += report_dialog
- report += report_link
-
- app = CTK.Box ({'class': 'market-app-desc'})
- app += CTK.Box ({'class': 'market-app-desc-icon'}, CTK.Image({'src': OWS_STATIC + info['icon_big']}))
- app += CTK.Box ({'class': 'market-app-desc-buy'}, buy)
- app += CTK.Box ({'class': 'market-app-desc-title'}, CTK.RawHTML(info['application_name']))
- app += CTK.Box ({'class': 'market-app-desc-version'}, CTK.RawHTML("%s: %s" %(_("Version"), info['version_string'])))
- app += CTK.Box ({'class': 'market-app-desc-url'}, by)
- app += CTK.Box ({'class': 'market-app-desc-category'}, CTK.RawHTML("%s: %s" %(_("Category"), info['category_name'])))
- app += CTK.Box ({'class': 'market-app-desc-short-desc'}, CTK.RawHTML(info['summary']))
- app += CTK.Box ({'class': 'market-app-desc-report'}, report)
- cont += app
-
+ report += report_dialog
+ report += report_link
+
+ # Info
+ repo_url = CTK.cfg.get_val ('admin!ows!repository', REPO_MAIN)
+ url_icon_big = os.path.join (repo_url, app['id'], "icons", app['icon_big'])
+
+ appw = CTK.Box ({'class': 'market-app-desc'})
+ appw += CTK.Box ({'class': 'market-app-desc-icon'}, CTK.Image({'src': url_icon_big}))
+ appw += CTK.Box ({'class': 'market-app-desc-buy'}, install_button)
+ appw += CTK.Box ({'class': 'market-app-desc-title'}, CTK.RawHTML(app['name']))
+ appw += CTK.Box ({'class': 'market-app-desc-version'}, CTK.RawHTML("%s: %s" %(_("Version"), app['version'])))
+ appw += CTK.Box ({'class': 'market-app-desc-url'}, by)
+ appw += CTK.Box ({'class': 'market-app-desc-packager'}, CTK.RawHTML("%s: %s" %(_("Packager"), maintainer['name'] or _("Orphan"))))
+ appw += CTK.Box ({'class': 'market-app-desc-category'}, CTK.RawHTML("%s: %s" %(_("Category"), app['category'])))
+ appw += CTK.Box ({'class': 'market-app-desc-short-desc'}, CTK.RawHTML(app['desc_short']))
+ appw += CTK.Box ({'class': 'market-app-desc-report'}, report)
+
+ # Support
ext_description = CTK.Box ({'class': 'market-app-desc-description'})
- ext_description += CTK.RawHTML(info['description'])
+ ext_description += CTK.RawHTML(app['desc_long'])
desc_panel = CTK.Box ({'class': 'market-app-desc-desc-panel'})
desc_panel += ext_description
- desc_panel += CTK.Box ({'class': 'market-app-desc-support-box'}, SupportBox(info))
-
- # Tabs
- tabs = CTK.Tab()
+ desc_panel += CTK.Box ({'class': 'market-app-desc-support-box'}, SupportBox(app_name))
# Shots
shots = CTK.CarouselThumbnails()
- shot_entries = info.get('shots', [])
+ shot_entries = app.get('screenshots', [])
if shot_entries:
for s in shot_entries:
- shots += CTK.Image ({'src': "%s/%s" %(OWS_STATIC, s)})
+ shots += CTK.Image ({'src': os.path.join (repo_url, app_name, "screenshots", s)})
else:
shots += CTK.Box ({'id': 'shot-box-empty'}, CTK.RawHTML ('<h2>%s</h2>' %(_("No screenshots"))))
+ # Tabs
+ tabs = CTK.Tab()
tabs.Add (_('Screenshots'), shots)
tabs.Add (_('Description'), desc_panel)
- app += tabs
-
- # Update the cache
- App.cache_app[str(app_id)] = info
-
- return cont.Render().toStr()
-
- def format_func_reviews (self, reviews_info):
- reviews = reviews_info['reviews']
- app_id = reviews_info['application_id']
- app_name = reviews_info['application_name']
-
- cont = CTK.Container()
- cont += CTK.RawHTML ("<h2>%s</h2>" %(_("Reviews")))
-
- # Render Reviews
- if not reviews:
- cont += CTK.Box ({'class': 'market-no-reviews'}, CTK.RawHTML(_("You can be the first one to review the application")))
- else:
- pags = CTK.Paginator('market-app-reviews', items_per_page=5)
- cont += pags
-
- for review in reviews:
- d, review_time = review['review_stamp'].value.split('T')
- review_date = "%s-%s-%s" %(d[0:4], d[4:6], d[6:])
-
- rev = CTK.Box ({'class': 'market-app-review'})
- rev += CTK.Box ({'class': 'market-app-review-score'}, CTK.StarRating({'selected': review['review_score']}))
- rev += CTK.Box ({'class': 'market-app-review-title'}, CTK.RawHTML(review['review_title']))
- rev += CTK.Box ({'class': 'market-app-review-name'}, CTK.RawHTML(review['first_name'] + ' ' + review['last_name']))
- rev += CTK.Box ({'class': 'market-app-review-stamp'}, CTK.RawHTML(_('on %(date)s at %(time)s' %({'date':review_date, 'time': review_time}))))
- rev += CTK.Box ({'class': 'market-app-review-comment'}, CTK.RawHTML(review['review_comment']))
- pags += rev
-
- # Review
- if OWS_Login.is_logged():
- CTK.cfg['tmp!market!review!app_id'] = str(app_id)
- CTK.cfg['tmp!market!review!app_name'] = app_name
-
- druid = CTK.Druid (CTK.RefreshableURL())
- dialog = CTK.Dialog ({'title':"%s: %s" %(_("Review"), app_name), 'width': 480})
- dialog += druid
- druid.bind ('druid_exiting', dialog.JS_to_close() + \
- self.xmlrpc_proxy_reviews.JS_to_refresh())
-
- add_review = CTK.Button(_("Review"), {'id':'button-review'})
- add_review.bind('click', dialog.JS_to_show() + \
- druid.JS_to_goto('"%s"'%(URL_REVIEW)))
-
- cont += dialog
- cont += add_review
- else:
- sign_box = CTK.Box ({'class': 'market-app-review-signin'})
-
- link = CTK.Link ('#', CTK.RawHTML (_("sign in")))
- link.bind ('click', self.login_dialog.JS_to_show())
-
- login_txt = CTK.Box()
- login_txt += CTK.RawHTML ("%s, " %(_('Please')))
- login_txt += link
- login_txt += CTK.RawHTML (" %s" %(_('to review the product')))
-
- sign_box += login_txt
- cont += sign_box
+ # GUI Layout
+ self += appw
+ self += tabs
+ self += install
- return cont.Render().toStr()
+class App:
def __call__ (self):
page = Page_Market_App (_('Application'))
- #Parse URL
+ # Figure app
tmp = re.findall ('^%s/(.+)$' %(URL_APP), CTK.request.url)
if not tmp:
page += CTK.RawHTML ('<h2>%s</h2>' %(_("Application not found")))
return page.Render()
+ app_name = tmp[0]
+
# Menu
self.menu = Menu([CTK.Link(URL_MAIN, CTK.RawHTML (_('Market Home')))])
+ page.mainarea += self.menu
referer = CTK.request.headers.get ('HTTP_REFERER', '')
- ref_search = re.findall ('%s/(.+)'%(URL_SEARCH), referer)
- ref_category = re.findall ('%s/(\d+)/(.+)'%(URL_CATEGORY), referer)
+ ref_search = re.findall ('%s/(.+)'%(URL_SEARCH), referer)
+ ref_category = re.findall ('%s/([\w ]+)'%(URL_CATEGORY), referer)
if ref_search:
self.menu += CTK.Link ('%s/%s'%(URL_SEARCH, ref_search[0]), CTK.RawHTML("%s %s" %(_('Search'), CTK.unescape_html(ref_search[0]))))
elif ref_category:
- self.menu += CTK.Link ('%s/%s/%s'%(URL_CATEGORY, ref_category[0][0], ref_category[0][1]), CTK.RawHTML("%s %s"%(_('Category'), CTK.unescape_html(ref_category[0][1]))))
+ self.menu += CTK.Link ('%s/%s'%(URL_CATEGORY, ref_category[0]), CTK.RawHTML("%s %s"%(_('Category'), CTK.unescape_html(ref_category[0]))))
- # Login dialog
- self.login_dialog = OWS_Login.LoginDialog()
- self.login_dialog.bind ('submit_success', CTK.JS.ReloadURL())
- page.mainarea += self.login_dialog
-
- # App info
- self.text_app_id = tmp[0]
- if App.cache_app.has_key(self.text_app_id):
- page.mainarea += CTK.RawHTML (self.format_func_app (App.cache_app[self.text_app_id]))
- else:
- self.xmlrpc_proxy_app = CTK.XMLRPCProxy (name = 'cherokee-market-app',
- xmlrpc_func = lambda: XmlRpcServer(OWS_APPS).get_full_app (self.text_app_id, OWS_Login.login_session),
- format_func = self.format_func_app,
- debug = OWS_DEBUG)
- page.mainarea += self.xmlrpc_proxy_app
-
- # App reviews
- self.xmlrpc_proxy_reviews = CTK.XMLRPCProxy (name = 'cherokee-market-app-reviews',
- xmlrpc_func = lambda: XmlRpcServer(OWS_APPS).list_app_reviews (self.text_app_id),
- format_func = self.format_func_reviews,
- debug = OWS_DEBUG)
- page.mainarea += self.xmlrpc_proxy_reviews
+ # App Info
+ page.mainarea += AppInfo (app_name)
+ # Final render
return page.Render()
73 admin/market/PageCategory.py
View
@@ -25,6 +25,7 @@
import re
import CTK
import Page
+import Distro
import OWS_Login
from consts import *
@@ -33,69 +34,63 @@
from Util import *
from Menu import Menu
-from XMLServerDigest import XmlRpcServer
-
class Categories_Widget (CTK.Box):
- cached = None
+ def __init__ (self):
+ CTK.Box.__init__ (self, {'class': 'cherokee-market-categories'})
+ index = Distro.Index()
- def format_func (self, info):
- # Format the list
+ # Build category list
+ category_names = []
+ for app_name in index.get('packages') or []:
+ cat_name = index.get_package (app_name, 'software').get('category')
+ if cat_name and not cat_name in category_names:
+ category_names.append (cat_name)
+
+ # Build Content
cat_list = CTK.List()
- for cat in info:
- link = CTK.Link ('%s/%s/%s' %(URL_CATEGORY, cat['category_id'], cat['category_name']), CTK.RawHTML(cat['category_name']))
+ for cat_name in category_names:
+ link = CTK.Link ('%s/%s' %(URL_CATEGORY, cat_name), CTK.RawHTML(cat_name))
cat_list += link
- # Save a copy
- render_str = cat_list.Render().toStr()
- if len(info) > 2:
- Categories_Widget.cached = render_str
- return render_str
-
- def __init__ (self):
- CTK.Box.__init__ (self, {'class': 'market-categories-list'})
+ # Layout
self += CTK.RawHTML ('<h3>%s</h3>' %(_('Categories')))
-
- if self.cached:
- self += CTK.RawHTML (self.cached)
- else:
- self += CTK.XMLRPCProxy (name = 'cherokee-market-categories',
- xmlrpc_func = lambda: XmlRpcServer(OWS_APPS).list_categories(),
- format_func = self.format_func,
- debug = OWS_DEBUG)
+ self += cat_list
class Category:
- def format_func (self, info):
- pags = CTK.Paginator('category-results', items_per_page=10)
- for app in info:
- pags += RenderApp (app)
-
- return pags.Render().toStr()
-
def __call__ (self):
+ index = Distro.Index()
page = Page_Market(_('Category'))
# Parse URL
- tmp = re.findall ('^%s/(\d+)/?(.*)$' %(URL_CATEGORY), CTK.request.url)
+ tmp = re.findall ('^%s/([\w ]+)$'%(URL_CATEGORY), CTK.request.url)
if not tmp:
page += CTK.RawHTML ('<h2>%s</h2>' %(_("Empty Category")))
return page.Render()
- self.categoty_num = tmp[0][0]
- self.category_name = tmp[0][1]
+ self.category_name = tmp[0]
# Menu
menu = Menu([CTK.Link(URL_MAIN, CTK.RawHTML (_('Market Home')))])
menu += "%s: %s" %(_('Category'), self.category_name)
page.mainarea += menu
- # Perform the search
- page.mainarea += CTK.XMLRPCProxy (name = 'cherokee-market-category',
- xmlrpc_func = lambda: XmlRpcServer(OWS_APPS).list_apps_in_category (self.categoty_num, OWS_Login.login_session),
- format_func = self.format_func,
- debug = OWS_DEBUG)
+ # Add apps
+ pags = CTK.Paginator('category-results', items_per_page=10)
+
+ box = CTK.Box ({'class': 'cherokee-market-category'})
+ box += pags
+ page.mainarea += box
+
+ for app_name in index.get('packages'):
+ app = index.get_package(app_name, 'software')
+ cat = app.get('category')
+ if cat == self.category_name:
+ pags += RenderApp (app)
+
+ # Render
return page.Render()
-CTK.publish ('^%s' %(URL_CATEGORY), Category)
+CTK.publish ('^%s' %(URL_CATEGORY), Category)
123 admin/market/PageIndex.py
View
@@ -26,6 +26,7 @@
import Library
import OWS_Login
import Maintenance
+import Distro
from Util import *
from consts import *
@@ -34,99 +35,91 @@
from XMLServerDigest import XmlRpcServer
-class FeaturedBox (CTK.Box):
- cache = []
+class RenderApp_Featured (CTK.Box):
+ def __init__ (self, info, props):
+ assert type(info) == dict
+ CTK.Box.__init__ (self, props.copy())
+
+ url_icon_big = '%s/%s/icons/%s' %(REPO_MAIN, info['id'], info['icon_big'])
+
+ self += CTK.Box ({'class': 'market-app-featured-icon'}, CTK.Image({'src': url_icon_big}))
+ self += CTK.Box ({'class': 'market-app-featured-name'}, CTK.Link ('%s/%s'%(URL_APP, info['id']), CTK.RawHTML(info['name'])))
+ self += CTK.Box ({'class': 'market-app-featured-category'}, CTK.RawHTML(info['category']))
+ self += CTK.Box ({'class': 'market-app-featured-summary'}, CTK.RawHTML(info['desc_short']))
+ self += CTK.Box ({'class': 'market-app-featured-score'}, CTK.StarRating({'selected': info.get('score', -1)}))
+
- def format_func (self, info):
- cont = CTK.Container()
+class FeaturedBox (CTK.Box):
+ def __init__ (self):
+ CTK.Box.__init__ (self, {'class': 'market-featured-box'})
+ index = Distro.Index()
# List
app_list = CTK.List ({'class': 'market-featured-box-list'})
app_content = CTK.Box ({'class': 'market-featured-box-content'})
- cont += app_content
- cont += app_list
+ self += app_content
+ self += app_list
app_content += CTK.RawHTML("<h2>%s</h2>" %(_('Featured Applications')))
# Build the list
- for app in info:
+ featured_apps = list(index.get('featured_apps') or [])
+ for app_id in featured_apps:
+ app = index.get_package (app_id, 'software')
+
# Content entry
props = {'class': 'market-app-featured',
- 'id': 'market-app-featured-%s' %(app['application_id'])}
+ 'id': 'market-app-featured-%s' %(app_id)}
- if info.index(app) != 0:
+ if featured_apps.index(app_id) != 0:
props['style'] = 'display:none'
app_content += RenderApp_Featured (app, props)
# List entry
+ url_icon_small = '%s/%s/icons/%s' %(REPO_MAIN, app['id'], app['icon_small'])
+
list_cont = CTK.Box ({'class': 'featured-list-entry'})
- list_cont += CTK.Box ({'class': 'featured-list-icon'}, CTK.Image({'src': OWS_STATIC + app['icon_small']}))
- list_cont += CTK.Box ({'class': 'featured-list-name'}, CTK.RawHTML(app['application_name']))
- list_cont += CTK.Box ({'class': 'featured-list-category'}, CTK.RawHTML(app['category_name']))
- if info.index(app) != 0:
- app_list.Add(list_cont, {'id': 'featured-list-%s' %(app['application_id'])})
+ list_cont += CTK.Box ({'class': 'featured-list-icon'}, CTK.Image({'src': url_icon_small}))
+ list_cont += CTK.Box ({'class': 'featured-list-name'}, CTK.RawHTML(app['name']))
+ list_cont += CTK.Box ({'class': 'featured-list-category'}, CTK.RawHTML(app['category']))
+ if featured_apps.index(app_id) != 0:
+ app_list.Add(list_cont, {'id': 'featured-list-%s' %(app_id)})
else:
- app_list.Add(list_cont, {'id': 'featured-list-%s' %(app['application_id']), 'class': 'selected'})
+ app_list.Add(list_cont, {'id': 'featured-list-%s' %(app_id), 'class': 'selected'})
list_cont.bind ('click',
"var list = $('.market-featured-box-list');" +
"list.find('li.selected').removeClass('selected');" +
- "list.find('#featured-list-%s').addClass('selected');" %(app['application_id']) +
+ "list.find('#featured-list-%s').addClass('selected');" %(app_id) +
"var content = $('.market-featured-box-content');" +
"content.find('.market-app-featured').hide();" +
- "content.find('#market-app-featured-%s').show();" %(app['application_id']))
+ "content.find('#market-app-featured-%s').show();" %(app_id))
- # Render and cache
- render = cont.Render().toStr()
- if len(info) > 2 and not self.cache:
- FeaturedBox.cache = info
- return render
+class TopApps (CTK.Box):
def __init__ (self):
- CTK.Box.__init__ (self, {'class': 'market-featured-box'})
-
- if self.cache:
- # Cache hit
- self += CTK.RawHTML (self.format_func(self.cache))
- else:
- # Cache miss
- self += CTK.XMLRPCProxy(name = 'cherokee-featured-box',
- xmlrpc_func = lambda: XmlRpcServer(OWS_APPS).get_featured_apps (OWS_Login.login_session),
- format_func = self.format_func,
- debug = OWS_DEBUG)
-
+ CTK.Box.__init__ (self, {'class': 'market-top-box'})
+ index = Distro.Index()
-class TopApps (CTK.Container):
- cache = {}
+ for app_name in index.get('top_apps') or []:
+ app = index.get_package(app_name, 'software')
+ self += RenderApp (app)
- def format_func (self, info):
- box = CTK.Box ({'class': 'market-top-box'})
- for app in info:
- box += RenderApp (app)
- render = box.Render().toStr()
- if len(info) > 2:
- self.cache[self.klass] = render
-
- return render
+class RepoError (CTK.Box):
+ def __init__ (self):
+ CTK.Box.__init__ (self, {'class': 'market-repo-error'})
- def __init__ (self, klass):
- CTK.Container.__init__ (self)
- self.klass = klass
+ repo_url = CTK.cfg.get_val ('admin!ows!repository', REPO_MAIN)
- if self.cache.has_key(klass):
- # Cache hit
- self += CTK.RawHTML (self.cache[klass])
- else:
- # Cache miss
- self += CTK.XMLRPCProxy(name = 'cherokee-market-category-%s'%(klass),
- xmlrpc_func = lambda: XmlRpcServer(OWS_APPS).get_top_apps (klass, OWS_Login.login_session),
- format_func = self.format_func,
- debug = OWS_DEBUG)
+ self += CTK.RawHTML ('<h2>%s</h2>'%(_("Could not access the repository")))
+ self += CTK.RawHTML ('<p>%s: ' %(_("Cherokee could not reach the remote package repository")))
+ self += CTK.LinkWindow (repo_url, CTK.RawHTML(repo_url))
+ self += CTK.RawHTML ('.</p>')
class Main:
@@ -137,20 +130,18 @@ def __call__ (self):
page = Page_Market()
+ # Check repository is accessible
+ index = Distro.Index()
+ if index.error:
+ page.mainarea += RepoError()
+ return page.Render()
+
# Featured
page.mainarea += FeaturedBox()
# Top
page.mainarea += CTK.RawHTML("<h2>%s</h2>" %(_('Top Applications')))
- tabs = CTK.Tab()
- tabs.Add (_('Paid'), TopApps('paid'))
- tabs.Add (_('Free'), TopApps('free'))
- tabs.Add (_('Popular'), TopApps('any'))
- page.mainarea += tabs
-
- # My Library
- if OWS_Login.is_logged():
- page.sidebar += Library.MyLibrary()
+ page.mainarea += TopApps()
# Maintanance
if Maintenance.does_it_need_maintenance():
44 admin/market/PageSearch.py
View
@@ -24,6 +24,7 @@
import re
import CTK
+import Distro
from consts import *
from ows_consts import *
@@ -48,47 +49,46 @@ def __init__ (self):
submit += CTK.TextField ({'name': 'search', 'optional_string': _('Search'), 'optional': True, 'class': 'filter'})
self += submit
-class Search:
- def format_func (self, info):
- pags = CTK.Paginator('search-results', items_per_page=10)
-
- # Title
- cont = CTK.Container()
- cont += CTK.RawHTML ("<h2>%d Result%s for: %s</h2>" %(len(info), ('','s')[len(info)>1], self.text_search))
- cont += pags
-
- # List of apps
- for app in info:
- pags += RenderApp (app)
-
- return cont.Render().toStr()
+class Search:
def __call__ (self):
page = Page_Market(_('Search Result'))
mainbox = CTK.Box ({'class': 'market-main-area'})
mainbox += CTK.RawHTML("<h1>%s</h1>" %(_('Market')))
-
# Parse the URL
tmp = re.findall ('^%s/(.+)$' %(URL_SEARCH), CTK.request.url)
if not tmp:
page += CTK.RawHTML ('<h2>%s</h2>' %(_("Empty Search")))
return page.Render()
- self.text_search = tmp[0]
+ search_term = tmp[0]
+ search_term_lower = search_term.lower()
# Menu
menu = Menu([CTK.Link(URL_MAIN, CTK.RawHTML (_('Market Home')))])
- menu += "%s %s"%(_("Search"), self.text_search)
+ menu += "%s %s"%(_("Search"), search_term)
page.mainarea += menu
# Perform the search
- page.mainarea += CTK.XMLRPCProxy (
- name = 'cherokee-market-search',
- xmlrpc_func = lambda: XmlRpcServer (OWS_APPS).lookup_application (CTK.util.to_unicode (self.text_search), OWS_Login.login_session),
- format_func = self.format_func,
- debug = OWS_DEBUG)
+ index = Distro.Index()
+ matches = []
+
+ for pkg_name in index.get_package_list():
+ pkg = index.get_package (pkg_name)
+ if search_term_lower in str(pkg).lower():
+ matches.append (pkg_name)
+
+ # Render
+ pags = CTK.Paginator('search-results', items_per_page=10)
+
+ page.mainarea += CTK.RawHTML ("<h2>%d Result%s for: %s</h2>" %(len(matches), ('','s')[len(matches)>1], search_term))
+ page.mainarea += pags
+
+ for app_name in matches:
+ app = index.get_package (app_name, 'software')
+ pags += RenderApp (app)
return page.Render()
11 admin/market/Report.py
View
@@ -68,16 +68,14 @@ def get_logs (app_id):
def Report_Apply():
app_id = CTK.post.get_val('app_id')
report = CTK.post.get_val('report')
- refund = CTK.post.get_val('refund')
app_logs = get_logs (app_id)
sysinfo = SystemInfo.get_info()
cfg = str(CTK.cfg)
- # OWS auth
- xmlrpc = XmlRpcServer(OWS_APPS_INSTALL, OWS_Login.login_user, OWS_Login.login_password)
+ # OWS Open
+ xmlrpc = XmlRpcServer(OWS_APPS_CENTER, OWS_Login.login_user, OWS_Login.login_password)
try:
ok = xmlrpc.report_application (app_id, # int
- bool(refund), # boolean
CTK.util.to_unicode(report), # string
CTK.util.to_unicode(app_logs), # list
CTK.util.to_unicode(sysinfo), # dict
@@ -117,12 +115,11 @@ def __call__ (self):
class Report:
def __call__ (self):
- application_id = CTK.cfg.get_val('tmp!market!report!app_id')
+ application_id = CTK.request.url.split('/')[-1]
# Build the content
submit = CTK.Submitter (URL_REPORT_APPLY)
submit += CTK.TextArea ({'name': 'report', 'rows':10, 'cols': 60, 'class': 'noauto'})
- submit += CTK.CheckboxText ({'name': 'refund', 'class': 'noauto'}, _('Request refund'))
submit += CTK.Hidden ('app_id', application_id)
submit.bind ('submit_fail', CTK.DruidContent__JS_to_goto (submit.id, URL_REPORT_FAIL))
submit.bind ('submit_success', CTK.DruidContent__JS_to_goto (submit.id, URL_REPORT_OK))
@@ -141,7 +138,7 @@ def __call__ (self):
return cont.Render().toStr()
-CTK.publish ('^%s$'%(URL_REPORT), Report)
+CTK.publish ('^%s'%(URL_REPORT), Report)
CTK.publish ('^%s$'%(URL_REPORT_FAIL), Report_Fail)
CTK.publish ('^%s$'%(URL_REPORT_OK), Report_Success)
CTK.publish ('^%s$'%(URL_REPORT_APPLY), Report_Apply, method="POST")
27 admin/market/Util.py
View
@@ -60,7 +60,7 @@ def __init__ (self, title=None):
# Top
from PageSearch import Search_Widget
top = CTK.Box({'id': 'top-box'})
- top += CTK.RawHTML ("<h1>%s</h1>"% _('Market'))
+ top += CTK.RawHTML ("<h1>%s</h1>"% _("Apps Center"))
top += Search_Widget()
# Sidebar
@@ -105,24 +105,15 @@ def __init__ (self, info):
assert type(info) == dict
CTK.Box.__init__ (self, {'class': 'market-app-entry'})
- self += CTK.Box ({'class': 'market-app-entry-icon'}, CTK.Image({'src': OWS_STATIC + info['icon_small']}))
- self += CTK.Box ({'class': 'market-app-entry-score'}, CTK.StarRating({'selected': info.get('score', -1)}))
- self += CTK.Box ({'class': 'market-app-entry-price'}, PriceTag(info))
- self += CTK.Box ({'class': 'market-app-entry-name'}, CTK.Link ('%s/%s'%(URL_APP, info['application_id']), CTK.RawHTML(info['application_name'])))
- self += CTK.Box ({'class': 'market-app-entry-category'}, CTK.RawHTML(info['category_name']))
- self += CTK.Box ({'class': 'market-app-entry-summary'}, CTK.RawHTML(info['summary']))
-
+ repo_url = CTK.cfg.get_val ('admin!ows!repository', REPO_MAIN)
+ url_icon_small = os.path.join (repo_url, info['id'], "icons", info['icon_small'])
-class RenderApp_Featured (CTK.Box):
- def __init__ (self, info, props):
- assert type(info) == dict
- CTK.Box.__init__ (self, props.copy())
-
- self += CTK.Box ({'class': 'market-app-featured-icon'}, CTK.Image({'src': OWS_STATIC + info['icon_big']}))
- self += CTK.Box ({'class': 'market-app-featured-name'}, CTK.Link ('%s/%s'%(URL_APP, info['application_id']), CTK.RawHTML(info['application_name'])))
- self += CTK.Box ({'class': 'market-app-featured-category'}, CTK.RawHTML(info['category_name']))
- self += CTK.Box ({'class': 'market-app-featured-summary'}, CTK.RawHTML(info['summary']))
- self += CTK.Box ({'class': 'market-app-featured-score'}, CTK.StarRating({'selected': info.get('score', -1)}))
+ self += CTK.Box ({'class': 'market-app-entry-icon'}, CTK.Image({'src': url_icon_small}))
+ self += CTK.Box ({'class': 'market-app-entry-score'}, CTK.StarRating({'selected': info.get('score', -1)}))
+ # self += CTK.Box ({'class': 'market-app-entry-price'}, PriceTag(info))
+ self += CTK.Box ({'class': 'market-app-entry-name'}, CTK.Link ('%s/%s'%(URL_APP, info['id']), CTK.RawHTML(info['name'])))
+ self += CTK.Box ({'class': 'market-app-entry-category'}, CTK.RawHTML(info['category']))
+ self += CTK.Box ({'class': 'market-app-entry-summary'}, CTK.RawHTML(info['desc_short']))
class PriceTag (CTK.Box):
3  admin/market/ows_consts.py
View
@@ -29,6 +29,7 @@
OWS_APPS = 'http://www.octality.com/api/v%s/open/market/apps/' %(OWS_API_VERSION)
OWS_APPS_AUTH = 'http://www.octality.com/api/v%s/market/apps/' %(OWS_API_VERSION)
OWS_APPS_INSTALL = 'http://www.octality.com/api/v%s/market/install/' %(OWS_API_VERSION)
+OWS_APPS_CENTER = 'http://www.octality.com/api/v%s/open/appscenter' %(OWS_API_VERSION)
OWS_DEBUG = True
@@ -39,3 +40,5 @@
URL_CATEGORY = '/market/category'
URL_REVIEW = '/market/review'
URL_REPORT = '/market/report'
+
+REPO_MAIN = 'http://www.cherokee-project.com/download/distribution'
22 admin/server.py
View
@@ -36,6 +36,7 @@
sys.path.append (os.path.abspath (os.path.realpath(__file__) + '/../CTK'))
import CTK
import OWS_Login
+import market.Distro
# Cherokee imports
import config_version
@@ -132,20 +133,12 @@ def trace_callback (sig, stack):
signal.signal (signal.SIGUSR2, trace_callback)
-def do_OWS_login():
- def thread_func (username, password):
- try:
- OWS_Login.log_in (username, password)
- except ProtocolError:
- # Do not give up so easily
- OWS_Login.log_in (username, password)
-
- username = CTK.cfg.get_val("admin!ows!login!user")
- password = CTK.cfg.get_val("admin!ows!login!password")
- ows_enable = int(CTK.cfg.get_val("admin!ows!enabled", OWS_ENABLE))
+def download_distro_index():
+ def thread_func():
+ # First instance will trigger the update
+ index = market.Distro.Index()
- if all((ows_enable, username, password)):
- thread.start_new_thread (thread_func, (username, password))
+ thread.start_new_thread (thread_func, ())
if __name__ == "__main__":
@@ -274,7 +267,8 @@ def are_vsrvs_num():
CTK.set_synchronous (False)
# Log into OWS if feature is enabled
- do_OWS_login()
+ ## do_OWS_login()
+ download_distro_index()
# Run forever
CTK.run()
10 admin/static/css/cherokee-admin.css
View
@@ -372,7 +372,7 @@ a.helpbutton span {
}
.check-list-flags-entry {
- padding: 4px 0;
+ padding: 4px 0;
}
.flags-buttons {
@@ -949,7 +949,7 @@ img.icon_chooser_selected {
.free { color: #0b0; }
-.market-app-desc-version, .market-app-desc-url, .market-app-desc-category {
+.market-app-desc-version, .market-app-desc-url, .market-app-desc-category, .market-app-desc-packager {
margin-left: 168px;
color: #666;
font-size: 90%;
@@ -1194,7 +1194,7 @@ img.icon_chooser_selected {
.mimeheader div {
padding: 8px;
- float: left;
+ float: left;
min-width: 32px;
margin-bottom: 8px;
}
@@ -1213,7 +1213,7 @@ img.icon_chooser_selected {
.mimeentry .md {
padding: 2px 8px;
- float: left;
+ float: left;
min-width: 32px;
margin-top: 4px;
}
@@ -1228,7 +1228,7 @@ img.icon_chooser_selected {
.md-del img { margin-top: 4px; }
.mime-button {
- margin-bottom: 64px;
+ margin-bottom: 64px;
}
/* Page Advanced */
2  admin/theme.html
View
@@ -15,7 +15,7 @@
<div id="save-box"><button id="save-button" class="button">%(save)s</button></div>
<ul id="nav">
<li id="nav-index"><a href="/">%(home)s</a></li>
- %(market_menu_entry)s
+ <li id="nav-market"><a href="/market">%(market)s</a></li>
<li id="nav-status"><a href="/status">%(status)s</a></li>
<li id="nav-general"><a href="/general">%(general)s</a></li>
<li id="nav-vservers"><a href="/vserver">%(vservers)s</a></li>
2  cherokee.conf.sample.pre
View
@@ -76,7 +76,7 @@ icons!suffix!page_white_office.png = doc,ppt,xls
icons!suffix!page_white_text.png = txt,text,rtf,sdw
icons!suffix!font.png = ttf
icons!suffix!music.png = au,snd,mid,midi,kar,mpga,mpega,mp2,mp3,sid,wav,aif,aiff,aifc,gsm,m3u,wma,wax,ra,rm,ram,pls,sd2,ogg
-icons!suffix!package.png = tar,gz,bz2,zip,rar,ace,lha,Z,7z,dmg
+icons!suffix!package.png = tar,gz,bz2,zip,rar,ace,lha,Z,7z,dmg,cpk
icons!suffix!film.png = avi,mpeg,mpe,mpg,mpeg3,dl,fli,qt,mov,movie,flv,webm
icons!suffix!cup.png = java,class,jar
icons!suffix!cd.png = iso,ngr,cue
3  doc/Makefile.am
View
@@ -132,7 +132,8 @@ dev_debug.html \
dev_cherokee.conf.html \
dev_qa.html \
dev_quickstart.html \
-dev_ctk.html
+dev_ctk.html \
+distro_dev_intro.html
docmediacssdir = $(docdir)/media/css/
docmediacss_DATA = \
390 doc/distro_dev_intro.txt
View
@@ -0,0 +1,390 @@
+= Building an installer
+
+Since release 1.2, Cherokee supports automatic application deployments. Packaging web applications is only a matter of knowing how to build an installer, which in turn is basically a Cherokee Wizard on steroids.
+
+To build an installer you’ll need some basic knowledge about the existing infrastructure, and also some rudimentary knowledge about CTK. Of course, some proficiency with Cherokee is required so that you can obtain a suitable configuration template, but that is pretty much it.
+
+== Installer
+
+A package is basically a tar.gz file containing a web application and some installation scripts. To build a package, at least the following files must be provided:
+. build.py: a definition file that contains the information required to download and build the package.
+. description.py: a definition file that provides the information about the package (name, release, revision, description, supported installation modes, etc).
+. installer.py: initial installation script on which Cherokee-Admin relies once it has downloaded the package to be installed.
+. Additionally, delta files to apply patches can be provided (to use while building the package), as well as other auxiliary installer files to be used by the main installer.py script. Also it is recommended to provide a file with notes about the installer or detailing a QA procedur to ensure the quality of the package.
+
+Lets detail the structure of both essential files and then we can proceed with some more specifics.
+
+=== build.py
+This must provide all the details required to successfully download, patch, and build the installer. The building change is actually quite sophisticated and will cache things whenever possible: it will not attempt to re-download source files, or even rebuild the package if
+.build.py
+-----
+# Remember to modify package revision when upgrading, so that build
+# system doesn't miss it
+REVISION = 25
+
+PY_DEPS = [
+
+'../common/php.py',
+ '../common/target.py',
+]
+
+PY_FILES = ['installer.py', 'tools.py']
+INCLUDE = ['bar']
+
+DOWNLOADS = [
+
+{'dir': 'bar',
+'mv’: (('Bar-v2.0.17’, 'bar’),),
+ 'url': 'http://example.com/downloads/Bar-v2.0.17.tar.bz2',
+ 'patches': [('patch-01-hide_db_block.diff', '-p1')]},
+
+ {'dir': 'bar/plugins', 'fence': True,
+ 'skip_if': 'bar/plugins/baz/flagged_file.php',
+ 'url': 'http://example.com/downloads/plugins/baz-1.x-dev.tar.gz'},
+]
+-----
+
+You can see several interesting properties on this file, which deserve more specific descriptions.
+
+==== PY_DEPS
+
+List of script dependencies. Basically intended to include the modules provided under the `common` directory. These are modules that take care of common functionality used extensively throughout all the installers.
+
+At the moment of writing, it contains the following modules.
+
+ - cc.py: Functionality related to detection and installation of a C development environment.
+ - cpp.py: Functionality related to detection and installation of a C++ development environment.
+ - database.py: Functionality related to detection and usage of several database engines.
+ - java.py: Functionality related to detection and installation of a JRE.
+ - php-mods.py: Functionality related to detection and installation of a PHP modules.
+ - php.py: Functionality related to detection and installation of a PHP interpreter.
+ - pwd_grp.py: Functionality related to system users and groups.
+ - python.py: Functionality related to detection and installation of Python.
+ - ruby.py: Functionality related to detection and installation of Ruby and Ruby Gems.
+ - services.py: Functionality related to system services.
+ - target.py: Module that defines target types for installations (Virtual server, Web directory).
+
+
+==== PY_FILES
+
+This is the list of scripts that must be packed with your installer (besides the ones specified as dependencies.
+In this list you can also specify files from other installers, which is the case when packing extended versions and you want to have a unique source to maintain when upgrading releases.
+
+==== INCLUDE
+
+This is the list of directories that should be included in the package. It is usually a good idea to specify the names after they have been renamed by the building script, so that you can avoid the need of having to rename directories manually every time you upgrade your package. More on this on the following entry.
+
+==== DOWNLOADS
+
+List of files that have to be downloaded from third party repositories. It is provided as a list of dictionaries with several keys, which globally allow you to define precisely what to download and where to put it. It also includes the directions required to apply patches and rename files/directories on demand, which is a good idea since by setting the directory names you can isolate the build script from the installer script.
+
+===== Download entry
+
+Each entry is a dictionary. Providing the `dir` and `url` elements is mandatory. All the rest are optional.
+
+----
+entry = {
+ 'dir': 'bar',
+ 'url': 'http://example.com/downloads/Bar-v2.0.17.tar.bz2',
+ 'mv’: [('Bar-v2.0.17’, 'bar’),],
+ 'patches': [('patch-01-hide_db_block.diff', '-p1')],
+ 'fence': True,
+ 'skip_if': 'bar/plugins/baz/foo.php',
+}
+----
+
+[cols="20%,80%",options="header"]
+|==================================================================
+|Key |Description
+|url |URL of the remote resource to download. Valid formats are tar.gz, tar.bz2, and zip.
+|dir |Directory where the file is unpacked. Should match an element of the INCLUDE property.
+|mv |Renaming list, should contain tuples indicating source and target names.
+|patches |List of patches to apply
+|fence |If True, create directory and change to it before decompression.
+|skip_if |If file or directory exists, skip download of this entry.
+|==================================================================
+
+=== description.py
+
+As has been mentioned, this file provides information about the package. The repository has to be indexed, and this is the source of such data.
+
+.description.py
+-----
+# Short summary to display in app-overview
+DESC_SHORT = """<p>This is the summary.</p>
+"""
+
+# Longer description to display in app-overview
+DESC_LONG = """<p>This is a longer description.</p>
+<p>You know the drill.</p>
+"""
+
+# Set software properties
+software = {
+ 'name': 'example', # part of the filename for the package
+ 'version': '0.9.9', # part of the filename for the package
+ 'author': 'Foo Bar, # appears in app-overview
+ 'URL': 'http://example.com/', # appears in app-overview
+ 'screenshots': ('shot1.png', 'shot2.png'), # files inside the screenshots directory to use
+ 'desc_short': DESC_SHORT,
+ 'desc_long': DESC_LONG,
+ 'category': 'Development', # group the app under this category
+}
+
+# Define the support table: installation modes, supported OSes, and
+# database engines
+installation = {
+ 'modes': ('vserver', 'webdir'),
+ 'OS': ('linux', 'macosx', 'freebsd', 'solaris'),
+ 'DB': ('mysql', 'postgresql', 'sqlite3')
+}
+
+# Info about the maintainer of the package
+maintainer = {
+ 'name': 'John Doe',
+ 'email': 'john@example.com
+}
+-----
+
+Everything is pretty self descriptive. The software dictionary should
+suffice to display information about the package within Cherokee
+Admin. It will be automatically put in place according to the category
+tag. Look for the one that better suits your app and use it, or create
+a new category if necessary.
+
+==== installation
+
+The installation properties define supported installation modes, supported Operating Systems, and supported database engines.
+These must be chosen among the following lists.
+
+.modes
+[cols="20%,80%",options="header"]
+|==================================================================
+|Value |Description
+|vserver |Can be installed as a new virtual server
+|webdir |Can be installed as a web-directory of an existing virtual server
+|==================================================================
+
+.OS
+[cols="20%,80%",options="header"]
+|==================================================================
+|Value |Description
+|linux |Linux support
+|macosx |MacOS X support
+|solaris |Solaris support
+|freebsd |FreeBSD support
+|==================================================================
+
+.DB
+[cols="20%,80%",options="header"]
+|==================================================================
+|Value |Description
+|mysql |MySQL engine supported by installer
+|postgresql |PostgreSQL engine supported by installer
+|sqlite3 |Sqlite3 engine supported by installer
+|==================================================================
+
+
+=== installer.py
+
+After downloading and decompressing a package, Cherokee Admin hands over the control to this script. It basically provides a configuration template, and follows the chain on installation stages until the common final installation stage is reached. Once this happens, Cherokee Admin will save and apply the configuration if possible, or will notify if pending changes remained before the installation so the step can be performed manually.
+
+Control is handed over by visiting a local URL that is used as entry point to the installer, and is returned when, upon completion, the installer visits another URL which is the exit point. In the middle, the chain of URLs must be published by the installer itself, that also has to establish the control flow between them.
+
+You are advised to make extensive use of the logging capabilities provided by market.Install_Log, so that it is easier to develop package-installers and report bugs if necessary.
+
+Have a look at the hello_world installer in the repository to see a thoroughly commented example.
+Here is an excerpt of a very basic example.
+
+.installer.py
+-----
+# Load external modules with CTK. Note that absolute paths are specified.
+target = CTK.load_module_pyc (os.path.join (os.path.dirname (os.path.realpath (__file__)), "target.pyo"), "target_util")
+
+# Batch commands, used to setup ownership and permissions, for example
+POST_UNPACK_COMMANDS = [
+ ({'command': 'chown -R root:${root_group} ${app_root}/package*'}),
+ ({'command': 'chmod 755 ${app_root}/package*'}),
+]
+
+# Cfg chunks
+CONFIG_VSERVER = """
+
+# Configuration snippet for virtual-server installations
+"""
+CONFIG_DIR = """
+
+# Configuration snippet for web-directory installations.
+"""
+NEXT_URL = "/market/install/example_app/config"
+
+## Step 1: Target
+class Target (Install_Stage):
+ def __safe_call__ (self):
+ box = CTK.Box()
+ target_wid = target.TargetSelection()
+ target_wid.bind ('goto_next_stage', CTK.DruidContent__JS_to_goto (box.id, NEXT_URL))
+ box += target_wid
+
+ buttons = CTK.DruidButtonsPanel()
+ buttons += CTK.DruidButton_Close(_('Cancel'))
+ buttons += CTK.DruidButton_Submit (_('Next'), do_close=False)
+ box += buttons
+ return box.Render().toStr()
+
+## Step 2: App configuration
+class App_Config (Install_Stage):
+ def __safe_call__ (self):
+ box = CTK.Box()
+ pre = 'tmp!market!install'
+
+ # Replacements
+ root = CTK.cfg.get_val ('%s!root' %(pre))
+ target_type = CTK.cfg.get_val ('%s!target' %(pre))
+
+ # Apply the config
+ if target_type == 'vserver':
+ config = CONFIG_VSERVER %(locals())
+ elif target_type == 'directory':
+ config = CONFIG_DIR %(locals())
+
+ CTK.cfg.apply_chunk (config)
+
+ # Redirect to the Thanks page
+ box += CTK.RawHTML (js = CTK.DruidContent__JS_to_goto (box.id, market.Install.URL_INSTALL_DONE))
+ return box.Render().toStr()
+
+CTK.publish ('^%s$'%(market.Install.URL_INSTALL_SETUP_EXTERNAL), Target)
+CTK.publish ('^%s$'%(NEXT_URL), App_Config)
+-----
+
+An overview of each section follows.
+
+==== Load of external modules
+This part is where you would import all the accessory modules into your current name space. Those are the ones with common functionality, or the ones you’ve added to the package besides build.py and installer.py.
+Note that absolute paths are specified on each import.
+
+====Batch commands: POST_UNPACK_COMMANDS
+This is a list of command entries.
+If this property is present in the installer script, the entries will be executed sequentially right after downloading and unpacking the application. When an error occurs the problem is notified within the installation screen. Otherwise, execution proceeds towards the first defined stage.
+
+The list of POST_UNPACK_COMMANDS is comprised of command entries that will be processed through an instance of market.CommandProgress.CommandProgress. Please refer to it later on this document for the specifics.
+
+==== market.Install
+You need to know market.Install.Install_Stage.
+Every installation stage inherits from this class, and will render the contents that need be displayed in the installation dialogs.
+
+The flow is determined by the mapped URLs/classes that are published by CTK in each installer.
+
+The entry point is *market.Install.URL_INSTALL_SETUP_EXTERNAL*. An hypothetical installer that wants to handle checks for dependencies through a class called Precondition that inherits from market.Install.Install_Stage, would map the URL to the class with the following code:
+
+.Mapping the entry point
+-----
+CTK.publish ('^%s$'%(market.Install.URL_INSTALL_SETUP_EXTERNAL), Precondition)
+-----
+
+Each stage must redirect the flow to the next URL, and each URL must be mapped to an Install_Stage with an analogous syntax. This mapping applies to every stage except the last one. Upon successful completion, the last Install_Stage should redirect the flow towards the standard exit point, *market.Install.URL_INSTALL_DONE*, which is already mapped
+
+=== DIFF files
+The build.py script can specify what patches have to be applied on build-time. Those are only applied if the entry specified in build.py is downloaded, so the caching mechanism for downloads and built packages remains consistent.
+
+It is recommended that these patches are generated using `diff -rcu` to maintain uniformity on the repositories and to provide context to the patches, possibly remaining valid after package upgrades.
+
+=== Important modules
+
+Some functionality is provided directly ith Cherokee Admin, which is what will be described in the following lines. Also, there are a series of common elements that can be reused by different installer (target selection, database management, handling PHP and PHP modules, etc.). As was mentioned before, such elements are encapsulated as independent modules distributed under the `common` directory, and are usually well comment, so don’t forget to review them before you begin working on your custom installers.
+
+==== popen
+popen.popen_sync is a function that provides a subset of the functionality of the subprocess.Popen class, but it does so consistently along all Python 2.x versions. Installers frequently need to execute command-line tasks, and this is the function that should be used. It accepts the following arguments.
+
+.popen_sync arguments
+[cols="10%,10%,30%,50%",options="header"]
+|==================================================================
+|Argument |Default |Example |Description
+|command |*mandatory* |chown root /tmp/foo |Command to execute
+|env |None |{'PATH’: "/usr/bin} |Provide custom environment
+|stdout |True |False |Include/exclude stdout in return value
+|stderr |True |False |Include/exclude stderr in return value
+|retcode |True |False |Include/exclude execution return code
+|cd |None |’/tmp/bar’ |Change to this directory before running
+|su |None |0 |Set process’s user id if possible
+|==================================================================
+
+
+==== market.Install_Log
+
+The most important function is market.Install_Log.log, which should be used extensively to log absolutely everything performed during an installation. File creation, task execution, and database modifications are logged in a file called `install.log` in the application’s directory.
+
+When maintenance tasks are launched to completely erase orphan or broken installations, this file is parsed to revert the modifications done to your system.
+
+It is also useful to log everything to ease up the troubleshooting process if anything goes wrong.
+
+==== market.CommandProgress
+
+The most relevant class is market.CommandProgress.CommandProgress.
+
+This class provides a way to process time consuming tasks in a sequential manner, without having to worry about the connection timing out.
+It is a CTK widget, and as such can be added to an existing container and will begin execution once it has been rendered. Provided a list of command entries, these will be executed sequentialy, after which the control flow will proceed to the URL specified when CommandProgress object was instanced. During execution, it will display a progress bar, and additionaly the command being executed or a provided description.
+
+Command entries are dictionaries with the following arguments, where specifying either a "command" or a "function" argument is mandatory.
+
+.Arguments
+[cols="15%,35%,50%",options="header"]
+|==================================================================
+|Argument |Default |Example |Description
+|command | |chown ${web_user} ${app_root} |Command to execute
+|function | |func_name |Function to execute
+|params |{} |{'arg1’: "hello"} |Arguments to pass to executed function
+|description |None |chown root /tmp/foo |Text to show instead of command or function name when command entry is being executed
+|env |None |{'PATH’: "/usr/bin} |Provide custom environment
+|cd |None |’/tmp/bar’ |Change to this directory before running
+|su |None |0 |Set process’s user id if possible
+|check_ret |True |False |Report error if return code is not 0.
+|==================================================================
+
+The commands being executed can have some replacement macros enclosed in curly braces and preceded by '$’ (i.e., ${macro}). Those will be substituted before actually running the command. The most usual ones are provided by default, and more can be added arbitrarily.
+
+.Replacement macros
+[cols="20%,80%",options="header"]
+|==================================================================
+|Macro |Description
+|web_user |User under which Cherokee runs
+|web_group |Group under which Cherokee runs
+|root_user |The system’s root user
+|root_group |The system’s root group (root, wheel, etc.)
+|app_root |Path where the downloaded application is decompressed
+|==================================================================
+
+Additional replacement macros can be specified when the CommandProgress in instanced manually, which is always except for the one that processes the POST_UNPACK_COMMANDS property.
+It is as simple as setting the macros as properties of the CommandProgress object.
+
+.Example: adding custom replacement macros
+----
+next_url = '/market/installer/example_app/final_stage’ # this can be set arbitrarily.
+commands = [{'command’: touch ${foo}’},]
+progress = market.CommandProgress.CommandProgress (commands, next_url)
+progress["path"] = '/tmp/bar’
+----
+
+
+==== market.Util
+
+The class market.Util.InstructionBox is used extensively, particularly by stages that check for preconditions that need be fulfilled in order to complete the installation of an application.
+
+It is a CTK widget used to display instructions to install software, tailored specifically to the users’ platform. It must be instanced with a mandatory note, such as "PHP not detected in your system.", and a dictionary (or list of dictionaries, for multi-message instructions) with messages for each platform.
+
+.Example
+----
+PHP_INSTRUCTIONS = {
+ 'apt': "sudo apt-get install php5-fpm or sudo apt-get install php5-cgi",
+ 'yum': "sudo yum install php",
+ 'zypper': "sudo zypper install php5-fastcgi",
+ 'macports': "sudo port install php5 +fastcgi",
+ 'freebsd_pkg': "pkg_add -r php5",
+ 'freebsd_ports': "cd /usr/ports/lang/php5 && make WITH_FASTCGI=yes WITH_FPM=yes install distclean",
+ 'ips': "pfexec pkg install 'pkg:/web/php-52'",
+ 'default': N_("PHP is available at http://www.php.net/ "),
+}
+----
+
+If instanced, the InstructionBox will determine which entry is apropriate for the user’s system, and show only that one (or fall through to the default entry and show that one if nothing more specific is found).
11 doc/index.txt
View
@@ -121,7 +121,7 @@ link:modules.html[Modules]: Information about the standard modules
- link:modules_balancers_failover.html[Failover]: Failover server strategy.
*********************************
-link:other.html[Other information]: Miscellaneus
+link:other.html[Other information]: Miscellaneous
*********************************
. link:other_faq.html[FAQ]: List of Frequently Asked Questions.
@@ -148,7 +148,14 @@ link:dev.html[Development info]: Things of interest to developers
. link:dev_debug.html[Debugging]: Resources available to debug Cherokee.
. link:dev_cherokee.conf.html[cherokee.conf]: Internal configuration specs.
. link:dev_qa.html[Quality Assurance]: Some info about QA in Cherokee.
- . link:dev_ctk.html[CTK]: Dive into the Cherokee ToolKit
+ . link:dev_ctk.html[CTK]: Dive into the Cherokee Toolkit
+
+*********************************
+Cherokee's Distribution: Web Apps distribution on Cherokee
+*********************************
+
+ . link:distro_dev_intro.html[Introduction to Development]: How to build a package.
+
////////////////////////////////////////////////////
Please sign in to comment.
Something went wrong with that request. Please try again.