- [[ repeatIn(lines(o.id),'line')]] [[ line['code'] ]]
+ [[ line['code'] ]]
[[ line['name'] ]]
@@ -365,10 +365,10 @@
+ [[ repeatIn(lines(o.id),'line')]]
[[ display_currency(data) or removeParentNode('blockTable') ]]
- [[lines(o.id) and removeParentNode('para') or removeParentNode('section') ]]
- [[ repeatIn(lines(o.id),'line')]] [[ line['code'] ]]
+ [[ line['code'] ]]
[[ line['name'] ]]
[[ formatLang(line['debit'] )]]
[[ formatLang(line['credit']) ]]
diff --git a/addons/account/report/account_general_ledger.rml b/addons/account/report/account_general_ledger.rml
index 5a3812206aa..7ded26849bd 100644
--- a/addons/account/report/account_general_ledger.rml
+++ b/addons/account/report/account_general_ledger.rml
@@ -245,7 +245,7 @@
Move
Entry Label
Debit
- Crebit
+ Credit
Balance
@@ -254,7 +254,7 @@
[[ data['form']['amount_currency'] == False or removeParentNode('blockTable') ]]
-
+
[[ '..'*(o.level-1) ]] [[ o.code or '']] [[ o.name or '']]
@@ -305,7 +305,7 @@
[[ data['form']['amount_currency'] == True or removeParentNode('blockTable') ]]
-
+
[[ '..'*(o.level-1) ]] [[ o.code or '' ]] [[ o.name or '' ]]
diff --git a/addons/account/report/account_general_ledger_landscape.rml b/addons/account/report/account_general_ledger_landscape.rml
index 80a55c76905..163d5cb5f2c 100644
--- a/addons/account/report/account_general_ledger_landscape.rml
+++ b/addons/account/report/account_general_ledger_landscape.rml
@@ -262,7 +262,7 @@
[[data['form']['amount_currency'] == True or removeParentNode('blockTable')]]
-
+
[[ '..'*(o.level-1) ]] [[ o.code ]] [[ o.name ]]
@@ -323,7 +323,7 @@
[[data['form']['amount_currency'] == False or removeParentNode('blockTable')]]
-
+
[[ '..'*(o.level-1) ]] [[ o.code ]] [[ o.name ]]
diff --git a/addons/account/report/account_journal.rml b/addons/account/report/account_journal.rml
index e153ae20adb..5b7ebf78766 100644
--- a/addons/account/report/account_journal.rml
+++ b/addons/account/report/account_journal.rml
@@ -252,7 +252,7 @@
Entry No
A/c No.
Partner
- Move - Entry label
+ Move/Entry label
Debit
Credit
Balance
@@ -264,8 +264,8 @@
[[ formatLang(sum_debit(o.period_id.id, o.journal_id.id)) ]]
- [[ formatLang(sum_credit(o.period_id.id, o.journal_id.id)) ]]
- [[ formatLang((sum_credit(o.period_id.id, o.journal_id.id) - sum_debit(o.period_id.id, o.journal_id.id))) ]] [[ company.currency_id.symbol ]]
+ [[ formatLang(sum_credit(o.period_id.id, o.journal_id.id)) ]]
+ [[ formatLang((sum_credit(o.period_id.id, o.journal_id.id)-sum_debit(o.period_id.id, o.journal_id.id))) ]] [[ company.currency_id.symbol ]]
[[lines(o.period_id.id, o.journal_id.id) or removeParentNode('story') ]]
@@ -287,7 +287,7 @@
Entry No
A/c No.
Partner
- Move - Entry label
+ Move/Entry label
Debit
Credit
Balance
diff --git a/addons/account/report/account_partner_balance.py b/addons/account/report/account_partner_balance.py
index 4ce8eeb986c..dcf35665a02 100644
--- a/addons/account/report/account_partner_balance.py
+++ b/addons/account/report/account_partner_balance.py
@@ -22,7 +22,7 @@
import time
import re
import copy
-
+from tools.translate import _
from report import report_sxw
from common_report_header import common_report_header
@@ -39,14 +39,7 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
'sum_debit': self._sum_debit,
'sum_credit': self._sum_credit,
'sum_litige': self._sum_litige,
- 'sum_sdebit': self._sum_sdebit,
- 'sum_scredit': self._sum_scredit,
- 'solde_debit': self._solde_balance_debit,
- 'solde_credit': self._solde_balance_credit,
- 'get_currency': self._get_currency,
- 'comma_me' : self.comma_me,
'get_fiscalyear': self._get_fiscalyear,
- 'get_periods':self.get_periods,
'get_journal': self._get_journal,
'get_filter': self._get_filter,
'get_account': self._get_account,
@@ -56,50 +49,8 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
'get_end_period': self.get_end_period,
'get_partners':self._get_partners,
})
- ## Compute account list one time
- #
- # Date Management
- #
-
- def get_periods(self, form):
- result = ''
- if form.has_key('periods') and form['periods']:
- period_ids = form['periods']
- per_ids = self.pool.get('account.period').browse(self.cr, self.uid, form['periods'])
- for r in per_ids:
- if r == per_ids[len(per_ids)-1]:
- result += r.name + ". "
- else:
- result += r.name + ", "
- else:
- fy_obj = self.pool.get('account.fiscalyear').browse(self.cr, self.uid, form['fiscalyear'])
- res = fy_obj.period_ids
- len_res = len(res)
- for r in res:
- if r == res[len_res-1]:
- result += r.name + ". "
- else:
- result += r.name + ", "
- return str(result and result[:-1]) or 'ALL'
-
- def comma_me(self, amount):
- if type(amount) is float :
- amount = str('%.2f'%amount)
- else :
- amount = str(amount)
- if (amount == '0'):
- return ' '
- orig = amount
- new = re.sub("^(-?\d+)(\d{3})", "\g<1>'\g<2>", amount)
- if orig == new:
- return new
- else:
- return self.comma_me(new)
def set_context(self, objects, data, ids, report_type=None):
- ## Compute Code
- #
-# self.initial_balance = data['form'].get('initial_balance', True)
self.display_partner = data['form'].get('display_partner', 'non-zero_balance')
self.query = data['form'].get('query_line', '')
self.init_query = data['form'].get('initial_bal_query', '')
@@ -156,63 +107,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
(self.ACCOUNT_TYPE, tuple(move_state)))
res = self.cr.dictfetchall()
- #For include intial balance..
-# if self.initial_balance:
-# self.cr.execute(
-# "SELECT '1' AS type, '' AS ref, l.account_id AS account_id, '' AS account_name, '' AS code, '' AS name, sum(debit) AS debit, sum(credit) AS credit, " \
-# "CASE WHEN sum(debit) > sum(credit) " \
-# "THEN sum(debit) - sum(credit) " \
-# "ELSE 0 " \
-# "END AS sdebit, " \
-# "CASE WHEN sum(debit) < sum(credit) " \
-# "THEN sum(credit) - sum(debit) " \
-# "ELSE 0 " \
-# "END AS scredit, " \
-# "(SELECT sum(debit-credit) " \
-# "FROM account_move_line l " \
-# "WHERE partner_id = p.id " \
-# " AND " + self.init_query + " " \
-# "AND blocked = TRUE " \
-# ") AS enlitige " \
-# "FROM account_move_line l LEFT JOIN res_partner p ON (l.partner_id=p.id) " \
-# "JOIN account_account ac ON (l.account_id = ac.id) " \
-# "WHERE ac.type IN %s " \
-# "AND " + self.init_query + "" \
-# "GROUP BY p.id, p.ref, p.name,l.account_id,ac.name,ac.code " \
-# "ORDER BY l.account_id, p.name",
-# (self.ACCOUNT_TYPE, ))
-# res1 = self.cr.dictfetchall()
-# final_init = {}
-# res_init = {}
-# debit = credit = 0
-# for r in res1:
-# if final_init.get(r['account_id'], False):
-# res_init = final_init[r['account_id']]
-# debit += final_init[r['account_id']]['debit']
-# credit += final_init[r['account_id']]['credit']
-# res_init['credit'] = credit
-# res_init['debit'] = debit
-# res_init['type'] = 3
-# res_init['ref'] = ''
-# res_init['code'] = ''
-# res_init['name'] = 'Initial Balance'
-# res_init['balance'] = debit - credit
-# res_init['enlitige'] = 0.0 # fix me
-# res_init['account_id'] = final_init[r['account_id']]['account_id']
-# else:
-# res_init = {}
-# debit = r['debit']
-# credit = r['credit']
-# res_init['credit'] = credit
-# res_init['debit'] = debit
-# res_init['type'] = 3
-# res_init['ref'] = ''
-# res_init['code'] = ''
-# res_init['name'] = 'Initial Balance'
-# res_init['balance'] = debit - credit
-# res_init['enlitige'] = 0.0 # fix me
-# res_init['account_id'] = r['account_id']
-# final_init[r['account_id']] = res_init
if self.display_partner == 'non-zero_balance':
full_account = [r for r in res if r['sdebit'] > 0 or r['scredit'] > 0]
@@ -221,20 +115,10 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
for rec in full_account:
if not rec.get('name', False):
- rec.update({'name': 'Unknown Partner'})
+ rec.update({'name': _('Unknown Partner')})
## We will now compute Total
subtotal_row = self._add_subtotal(full_account)
-# if not self.initial_balance:
-# return subtotal_row
-#
-# #If include initial balance is selected..
-# subtotal = copy.deepcopy(subtotal_row)
-# init_acnt = []
-# for row in subtotal_row:
-# if final_init and row.get('account_id', False) and not row['account_id'] in init_acnt and final_init.get(row['account_id'], False):
-# subtotal.insert(subtotal.index(row), final_init[row['account_id']])
-# init_acnt.append(row['account_id'])
return subtotal_row
def _add_subtotal(self, cleanarray):
@@ -257,12 +141,12 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
new_header['ref'] = ''
new_header['name'] = r['account_name']
new_header['code'] = r['code']
- new_header['debit'] = r['credit'] #tot_credit
- new_header['credit'] = r['debit'] #tot_debit
+ new_header['debit'] = r['debit']
+ new_header['credit'] = r['credit']
new_header['scredit'] = tot_scredit
new_header['sdebit'] = tot_sdebit
new_header['enlitige'] = tot_enlitige
- new_header['balance'] = float(tot_sdebit) - float(tot_scredit)
+ new_header['balance'] = r['debit'] - r['credit']
new_header['type'] = 3
##
completearray.append(new_header)
@@ -371,14 +255,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
"AND " + self.query + "" ,
(tuple(self.account_ids), tuple(move_state)))
temp_res = float(self.cr.fetchone()[0] or 0.0)
-# if self.initial_balance:
-# self.cr.execute(
-# "SELECT sum(debit) " \
-# "FROM account_move_line AS l " \
-# "WHERE l.account_id IN %s " \
-# "AND " + self.init_query + "" ,
-# (tuple(self.account_ids), ))
-# temp_res += float(self.cr.fetchone()[0] or 0.0)
return temp_res
def _sum_credit(self):
@@ -398,14 +274,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
"AND " + self.query + "" ,
(tuple(self.account_ids), tuple(move_state)))
temp_res = float(self.cr.fetchone()[0] or 0.0)
-# if self.initial_balance:
-# self.cr.execute(
-# "SELECT sum(credit) " \
-# "FROM account_move_line AS l " \
-# "WHERE l.account_id IN %s " \
-# "AND " + self.init_query + "" ,
-# (tuple(self.account_ids),))
-# temp_res += float(self.cr.fetchone()[0] or 0.0)
return temp_res
def _sum_litige(self):
@@ -426,82 +294,8 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
"AND l.blocked=TRUE ",
(tuple(self.account_ids), tuple(move_state), ))
temp_res = float(self.cr.fetchone()[0] or 0.0)
-# if self.initial_balance:
-# self.cr.execute(
-# "SELECT sum(debit-credit) " \
-# "FROM account_move_line AS l " \
-# "WHERE l.account_id IN %s " \
-# "AND l.blocked=TRUE "\
-# "AND " + self.init_query + "" ,
-# (tuple(self.account_ids), ))
-# temp_res += float(self.cr.fetchone()[0] or 0.0)
return temp_res
- def _sum_sdebit(self):
- move_state = ['draft','posted']
- if self.target_move == 'posted':
- move_state = ['posted']
-
- if not self.ids:
- return 0.0
- result_tmp = 0.0
- a = 0.0
- self.cr.execute(
- "SELECT CASE WHEN sum(debit) > sum(credit) " \
- "THEN sum(debit) - sum(credit) " \
- "ELSE 0 " \
- "END " \
- "FROM account_move_line AS l " \
- "JOIN account_move am ON (am.id = l.move_id)" \
- "WHERE l.account_id IN %s" \
- "AND am.state IN %s" \
- "AND " + self.query + " " \
- "GROUP BY l.partner_id",
- (tuple(self.account_ids), tuple(move_state),))
- a = self.cr.fetchone()[0]
-
- if self.cr.fetchone() != None:
- result_tmp = result_tmp + (a or 0.0)
- else:
- result_tmp = 0.0
- return result_tmp
-
- def _sum_scredit(self):
- move_state = ['draft','posted']
- if self.target_move == 'posted':
- move_state = ['posted']
-
- if not self.ids:
- return 0.0
- result_tmp = 0.0
- a = 0.0
- self.cr.execute(
- "SELECT CASE WHEN sum(debit) < sum(credit) " \
- "THEN sum(credit) - sum(debit) " \
- "ELSE 0 " \
- "END " \
- "FROM account_move_line AS l " \
- "WHERE l.account_id IN %s" \
- "JOIN account_move am ON (am.id = l.move_id)" \
- "AND am.state IN %s" \
- "AND " + self.query + " " \
- "GROUP BY l.partner_id",
- (tuple(self.account_ids), tuple(move_state), ))
- a = self.cr.fetchone()[0] or 0.0
- if self.cr.fetchone() != None:
- result_tmp = result_tmp + (a or 0.0)
- else:
- result_tmp = 0.0
- return result_tmp
-
- def _solde_balance_debit(self):
- debit, credit = self._sum_debit(), self._sum_credit()
- return debit > credit and debit - credit
-
- def _solde_balance_credit(self):
- debit, credit = self._sum_debit(), self._sum_credit()
- return credit > debit and credit - debit
-
def _get_partners(self):
if self.result_selection == 'customer':
return 'Receivable Accounts'
@@ -513,4 +307,4 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
report_sxw.report_sxw('report.account.partner.balance', 'res.partner', 'account/report/account_partner_balance.rml',parser=partner_balance, header="internal")
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/account/report/account_partner_balance.rml b/addons/account/report/account_partner_balance.rml
index a11df4e7576..eee31e87d6c 100644
--- a/addons/account/report/account_partner_balance.rml
+++ b/addons/account/report/account_partner_balance.rml
@@ -1,5 +1,5 @@
-
+
@@ -234,14 +234,14 @@
Debit
Credit
Balance
- In dispute
+ In dispute
Total:
- [[ formatLang(sum_debit()) ]]
+ [[ formatLang(sum_debit()) ]]
[[ formatLang(sum_credit()) ]]
- [[ formatLang((solde_debit()-solde_credit())) ]] [[ company.currency_id.symbol ]]
+ [[ formatLang((sum_debit()-sum_credit())) ]] [[ company.currency_id.symbol ]]
[[ formatLang(sum_litige()) ]] [[ company.currency_id.symbol ]]
diff --git a/addons/account/wizard/account_chart.py b/addons/account/wizard/account_chart.py
index bf10838b9c8..b8e18659327 100644
--- a/addons/account/wizard/account_chart.py
+++ b/addons/account/wizard/account_chart.py
@@ -44,14 +44,6 @@ class account_chart(osv.osv_memory):
fiscalyears = self.pool.get('account.fiscalyear').search(cr, uid, [('date_start', '<', now), ('date_stop', '>', now)], limit=1 )
return fiscalyears and fiscalyears[0] or False
- def _build_periods(self, cr, uid, period_from, period_to):
- period_obj = self.pool.get('account.period')
- period_date_start = period_obj.read(cr, uid, period_from, ['date_start'])['date_start']
- period_date_stop = period_obj.read(cr, uid, period_to, ['date_stop'])['date_stop']
- if period_date_start > period_date_stop:
- raise osv.except_osv(_('Error'),_('Start period should be smaller then End period'))
- return period_obj.search(cr, uid, [('date_start', '>=', period_date_start), ('date_stop', '<=', period_date_stop)])
-
def onchange_fiscalyear(self, cr, uid, ids, fiscalyear_id=False, context=None):
res = {}
res['value'] = {}
@@ -95,9 +87,9 @@ class account_chart(osv.osv_memory):
result = mod_obj._get_id(cr, uid, 'account', 'action_account_tree')
id = mod_obj.read(cr, uid, [result], ['res_id'], context=context)[0]['res_id']
result = act_obj.read(cr, uid, [id], context=context)[0]
- result['periods'] = []
+ result['periods'] = []
if data['period_from'] and data['period_to']:
- result['periods'] = self._build_periods(cr, uid, data['period_from'], data['period_to'])
+ result['periods'] = self.pool.get('account.period').build_ctx_periods(cr, uid, data['period_from'], data['period_to'])
result['context'] = str({'fiscalyear': data['fiscalyear'], 'periods': result['periods'], \
'state': data['target_move']})
if data['fiscalyear']:
@@ -111,4 +103,4 @@ class account_chart(osv.osv_memory):
account_chart()
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/account/wizard/account_report_common.py b/addons/account/wizard/account_report_common.py
index e5ed39427b4..a9a343a0d6a 100644
--- a/addons/account/wizard/account_report_common.py
+++ b/addons/account/wizard/account_report_common.py
@@ -103,14 +103,6 @@ class account_common_report(osv.osv_memory):
'chart_account_id': _get_account,
}
- def _build_periods(self, cr, uid, period_from, period_to):
- period_obj = self.pool.get('account.period')
- period_date_start = period_obj.read(cr, uid, period_from, ['date_start'])['date_start']
- period_date_stop = period_obj.read(cr, uid, period_to, ['date_stop'])['date_stop']
- if period_date_start > period_date_stop:
- raise osv.except_osv(_('Error'),_('Start period should be smaller then End period'))
- return period_obj.search(cr, uid, [('date_start', '>=', period_date_start), ('date_stop', '<=', period_date_stop)])
-
def _build_contexts(self, cr, uid, ids, data, context=None):
if context is None:
context = {}
@@ -129,9 +121,10 @@ class account_common_report(osv.osv_memory):
elif data['form']['filter'] == 'filter_period':
if not data['form']['period_from'] or not data['form']['period_to']:
raise osv.except_osv(_('Error'),_('Select a starting and an ending period'))
- result['periods'] = self._build_periods(cr, uid, data['form']['period_from'], data['form']['period_to'])
- first_period = self.pool.get('account.period').search(cr, uid, [], order='date_start', limit=1)[0]
- result_initial_bal['periods'] = self._build_periods(cr, uid, first_period, data['form']['period_from'])
+ company_id = period_obj.browse(cr, uid, data['form']['period_from'], context=context).company_id.id
+ result['periods'] = period_obj.build_ctx_periods(cr, uid, data['form']['period_from'], data['form']['period_to'])
+ first_period = self.pool.get('account.period').search(cr, uid, [('company_id', '=', company_id)], order='date_start', limit=1)[0]
+ result_initial_bal['periods'] = period_obj.build_ctx_periods(cr, uid, first_period, data['form']['period_from'])
else:
if data['form']['fiscalyear_id']:
fiscal_date_start = fiscal_obj.browse(cr, uid, [data['form']['fiscalyear_id']], context=context)[0].date_start
diff --git a/addons/account_budget/crossovered_budget.py b/addons/account_budget/crossovered_budget.py
index e0e1be5e2a2..6c7eb6639ec 100644
--- a/addons/account_budget/crossovered_budget.py
+++ b/addons/account_budget/crossovered_budget.py
@@ -18,6 +18,7 @@
# along with this program. If not, see .
#
##############################################################################
+
import datetime
from osv import osv, fields
@@ -42,7 +43,7 @@ class account_budget_post(osv.osv):
'company_id': fields.many2one('res.company', 'Company', required=True),
}
_defaults = {
- 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.budget.post', context=c)
+ 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.budget.post', context=c)
}
_order = "name"
@@ -62,8 +63,8 @@ class account_budget_post(osv.osv):
account_budget_post()
class account_budget_post_dotation(osv.osv):
- def _tot_planned(self, cr, uid, ids,name,args,context):
- res={}
+ def _tot_planned(self, cr, uid, ids, name, args, context):
+ res = {}
for line in self.browse(cr, uid, ids):
if line.period_id:
obj_period=self.pool.get('account.period').browse(cr, uid, line.period_id.id)
@@ -75,7 +76,6 @@ class account_budget_post_dotation(osv.osv):
OR (date_to >=%s and date_to <= %s) OR (date_from < %s and date_to > %s)"
cr.execute(query,(budget_id,obj_period.date_start,obj_period.date_stop,obj_period.date_start,obj_period.date_stop,obj_period.date_start,obj_period.date_stop,))
res1=cr.fetchall()
-
tot_planned=0.00
for record in res1:
obj_lines = self.pool.get('crossovered.budget.lines').browse(cr, uid, record[0])
@@ -84,9 +84,9 @@ class account_budget_post_dotation(osv.osv):
count_days = strToDate(obj_lines.date_to) - strToDate(obj_lines.date_from)
total_days_of_rec = count_days.days +1
tot_planned += obj_lines.planned_amount/total_days_of_rec* days_in_period
- res[line.id]=tot_planned
+ res[line.id] = tot_planned
else:
- res[line.id]=0.00
+ res[line.id] = 0.00
return res
_name = 'account.budget.post.dotation'
@@ -143,7 +143,6 @@ class crossovered_budget(osv.osv):
return True
def budget_cancel(self, cr, uid, ids, *args):
-
self.write(cr, uid, ids, {
'state':'cancel'
})
@@ -165,7 +164,6 @@ class crossovered_budget_lines(osv.osv):
acc_ids = [x.id for x in line.general_budget_id.account_ids]
if not acc_ids:
raise osv.except_osv(_('Error!'),_("The General Budget '%s' has no Accounts!") % str(line.general_budget_id.name))
-
date_to = line.date_to
date_from = line.date_from
if context.has_key('wizard_date_from'):
@@ -185,7 +183,6 @@ class crossovered_budget_lines(osv.osv):
res={}
for line in self.browse(cr, uid, ids):
res[line.id]=self._prac_amt(cr, uid, [line.id], context=context)[line.id]
-
return res
def _theo_amt(self, cr, uid, ids,context={}):
@@ -233,7 +230,8 @@ class crossovered_budget_lines(osv.osv):
else:
res[line.id]=0.00
return res
- _name="crossovered.budget.lines"
+
+ _name = "crossovered.budget.lines"
_description = "Budget Line"
_columns = {
'crossovered_budget_id': fields.many2one('crossovered.budget', 'Budget', ondelete='cascade', select=True, required=True),
@@ -252,7 +250,6 @@ class crossovered_budget_lines(osv.osv):
crossovered_budget_lines()
class account_analytic_account(osv.osv):
- _name = 'account.analytic.account'
_inherit = 'account.analytic.account'
_columns = {
diff --git a/addons/account_budget/crossovered_budget_view.xml b/addons/account_budget/crossovered_budget_view.xml
index 263dee3f686..7a4883ec61d 100644
--- a/addons/account_budget/crossovered_budget_view.xml
+++ b/addons/account_budget/crossovered_budget_view.xml
@@ -266,8 +266,8 @@
id="act_account_analytic_account_cb_lines"/>
-
- account.analytic.account.form.inherot.cci
+
+ account.analytic.account.form.inherot.budget
form
account.analytic.account
diff --git a/addons/account_budget/report/__init__.py b/addons/account_budget/report/__init__.py
index 085b4262b6c..d1104982f8c 100644
--- a/addons/account_budget/report/__init__.py
+++ b/addons/account_budget/report/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL ().
#
@@ -15,12 +15,13 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
import crossovered_budget_report
import analytic_account_budget_report
import budget_report
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/account_budget/report/analytic_account_budget_report.py b/addons/account_budget/report/analytic_account_budget_report.py
index 37fc4680d1f..c724968a06d 100644
--- a/addons/account_budget/report/analytic_account_budget_report.py
+++ b/addons/account_budget/report/analytic_account_budget_report.py
@@ -20,9 +20,10 @@
##############################################################################
import time
+import datetime
+
import pooler
from report import report_sxw
-import datetime
class analytic_account_budget_report(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context):
@@ -34,7 +35,6 @@ class analytic_account_budget_report(report_sxw.rml_parse):
})
self.context=context
-
def funct(self,object,form,ids={}, done=None, level=1):
if not ids:
@@ -181,5 +181,5 @@ class analytic_account_budget_report(report_sxw.rml_parse):
return result
report_sxw.report_sxw('report.account.analytic.account.budget', 'account.analytic.account', 'addons/account_budget/report/analytic_account_budget_report.rml',parser=analytic_account_budget_report,header='internal')
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/account_budget/report/analytic_account_budget_report.rml b/addons/account_budget/report/analytic_account_budget_report.rml
index e349b3d4920..ea800685470 100644
--- a/addons/account_budget/report/analytic_account_budget_report.rml
+++ b/addons/account_budget/report/analytic_account_budget_report.rml
@@ -104,7 +104,7 @@
Currency: [[ company.currency_id.name ]]
- Printed at: [[ formatLang(time.strftime('%Y-%m-%d'),date=True) ]] at [[ time.strftime('%H:%M:%S')]]
+ Printed at: [[ formatLang(time.strftime('%Y-%m-%d'),date=True) ]] at [[ time.strftime('%H:%M:%S')]]
@@ -186,4 +186,4 @@
-
\ No newline at end of file
+
diff --git a/addons/account_budget/report/budget_report.py b/addons/account_budget/report/budget_report.py
index 648015b3d2b..171984e9851 100644
--- a/addons/account_budget/report/budget_report.py
+++ b/addons/account_budget/report/budget_report.py
@@ -20,8 +20,9 @@
##############################################################################
import time
-from report import report_sxw
import datetime
+
+from report import report_sxw
import operator
class budget_report(report_sxw.rml_parse):
@@ -64,8 +65,8 @@ class budget_report(report_sxw.rml_parse):
achievements = float(self.cr.fetchone()[0])
res.append({'name': a.name, 'code': a.code, 'achievements': achievements})
return res
+
report_sxw.report_sxw('report.account.budget', 'account.budget.post', 'addons/account_budget/report/budget_report.rml',parser=budget_report, header="internal")
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/account_budget/wizard/account_budget_analytic.py b/addons/account_budget/wizard/account_budget_analytic.py
index 62553520cbd..70ce1a5156b 100644
--- a/addons/account_budget/wizard/account_budget_analytic.py
+++ b/addons/account_budget/wizard/account_budget_analytic.py
@@ -53,5 +53,4 @@ class account_budget_analytic(osv.osv_memory):
account_budget_analytic()
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/account_budget/wizard/account_budget_crossovered_report.py b/addons/account_budget/wizard/account_budget_crossovered_report.py
index 2862cd1db48..2427044e681 100644
--- a/addons/account_budget/wizard/account_budget_crossovered_report.py
+++ b/addons/account_budget/wizard/account_budget_crossovered_report.py
@@ -18,6 +18,7 @@
# along with this program. If not, see .
#
##############################################################################
+
import time
from osv import fields, osv
@@ -54,5 +55,4 @@ class account_budget_crossvered_report(osv.osv_memory):
account_budget_crossvered_report()
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/account_budget/wizard/account_budget_spread.py b/addons/account_budget/wizard/account_budget_spread.py
index b5ab413a862..31e059f754b 100644
--- a/addons/account_budget/wizard/account_budget_spread.py
+++ b/addons/account_budget/wizard/account_budget_spread.py
@@ -18,7 +18,7 @@
# along with this program. If not, see .
#
##############################################################################
-import netsvc
+
from osv import fields, osv
import decimal_precision as dp
diff --git a/addons/account_coda/__openerp__.py b/addons/account_coda/__openerp__.py
index 4aa61c20827..660103e878a 100644
--- a/addons/account_coda/__openerp__.py
+++ b/addons/account_coda/__openerp__.py
@@ -28,7 +28,7 @@
Module provides functionality to import
bank statements from coda files.
""",
- "depends" : ["account"],
+ "depends" : ["account_voucher"],
"demo_xml" : [],
"init_xml" : [],
"update_xml": ["security/ir.model.access.csv",
diff --git a/addons/account_coda/wizard/account_coda_import.py b/addons/account_coda/wizard/account_coda_import.py
index 913db0bc4c2..8a88f4a255d 100644
--- a/addons/account_coda/wizard/account_coda_import.py
+++ b/addons/account_coda/wizard/account_coda_import.py
@@ -48,7 +48,7 @@ class account_coda_import(osv.osv_memory):
'journal_id': fields.many2one('account.journal', 'Bank Journal', required=True),
'def_payable': fields.many2one('account.account', 'Default Payable Account', domain=[('type', '=', 'payable')], required=True, help= 'Set here the payable account that will be used, by default, if the partner is not found'),
'def_receivable': fields.many2one('account.account', 'Default Receivable Account', domain=[('type', '=', 'receivable')], required=True, help= 'Set here the receivable account that will be used, by default, if the partner is not found',),
- 'awaiting_account': fields.many2one('account.account', 'Default Account for Unrecognized Movement', required=True, help= 'Set here the default account that will be used, if the partner is found but does not have the bank account , or if he is domiciled',),
+ 'awaiting_account': fields.many2one('account.account', 'Default Account for Unrecognized Movement', domain=[('type', '=', 'liquidity')], required=True, help= 'Set here the default account that will be used, if the partner is found but does not have the bank account , or if he is domiciled'),
'coda': fields.binary('Coda File', required=True),
'note':fields.text('Log'),
}
@@ -61,7 +61,8 @@ class account_coda_import(osv.osv_memory):
bank_statement_obj = self.pool.get('account.bank.statement')
move_line_obj = self.pool.get('account.move.line')
bank_statement_line_obj = self.pool.get('account.bank.statement.line')
- statement_reconcile_obj = self.pool.get('account.bank.statement.reconcile')
+ voucher_obj = self.pool.get('account.voucher')
+ voucher_line_obj = self.pool.get('account.voucher.line')
account_coda_obj = self.pool.get('account.coda')
mod_obj = self.pool.get('ir.model.data')
@@ -194,48 +195,76 @@ class account_coda_import(osv.osv_memory):
for statement in bank_statements:
try:
bk_st_id =bank_statement_obj.create(cr, uid, {
- 'journal_id': statement['journal_id'],
- 'date': time.strftime('%Y-%m-%d', time.strptime(statement['date'], "%y/%m/%d")),
- 'period_id': statement['period_id'] or period,
- 'balance_start': statement["balance_start"],
- 'balance_end_real': statement["balance_end_real"],
+ 'journal_id': statement.get('journal_id',False),
+ 'date': time.strftime('%Y-%m-%d', time.strptime(statement.get('date',time.strftime('%Y-%m-%d')), "%y/%m/%d")),
+ 'period_id': statement.get('period_id',False) or period,
+ 'balance_start': statement.get('balance_start',False),
+ 'balance_end_real': statement.get('balance_end_real',False),
'state': 'draft',
- 'name': statement['name'],
+ 'name': statement.get('name',False),
})
- lines=statement["bank_statement_line"]
- for value in lines:
- line=lines[value]
- reconcile_id = False
- if line['toreconcile']:
- name = line['name'][:3] + '/' + line['name'][3:7] + '/' + line['name'][7:]
- rec_id = pool.get('account.move.line').search(cr, uid, [('name', '=', name), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)])
- if rec_id:
- reconcile_id = statement_reconcile_obj.create(cr, uid, {
- 'line_ids': [(6, 0, rec_id)]
- }, context=context)
- mv = pool.get('account.move.line').browse(cr, uid, rec_id[0], context=context)
- if mv.partner_id:
- line['partner_id'] = mv.partner_id.id
- if line['amount'] < 0 :
- line['account_id'] = mv.partner_id.property_account_payable.id
- else :
- line['account_id'] = mv.partner_id.property_account_receivable.id
+ lines = statement.get('bank_statement_line',False)
+ if lines:
+ for value in lines:
+ line = lines[value]
+ voucher_id = False
+ if line.get('toreconcile',False): # Fix me
+ name = line['name'][:3] + '/' + line['name'][3:7] + '/' + line['name'][7:]
+ rec_id = self.pool.get('account.move.line').search(cr, uid, [('name', '=', name), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)])
+ if rec_id:
+ voucher_res = { 'type':(line['amount'] < 0 and 'payment' or 'receipt') ,
+ 'name': line['name'],#line.name,
+ 'partner_id': line['partner_id'] ,#line.partner_id.id,
+ 'journal_id': statement['journal_id'], #statement.journal_id.id,
+ 'account_id': line['account_id'],#line.account_id.id,
+ 'company_id': statement['company_id'],#statement.company_id.id,
+ 'currency_id': statement['currency'],#statement.currency.id,
+ 'date': line['date'], #line.date,
+ 'amount':abs(line['amount']),
+ 'period_id':statement.get('period_id',False) or period,# statement.period_id.id
+ }
+ voucher_id = voucher_obj.create(cr, uid, voucher_res, context=context)
+
+ result = voucher_obj.onchange_partner_id(cr, uid, [], partner_id=line.partner_id.id, journal_id=statement.journal_id.id, price=abs(amount), currency_id= statement.currency.id, ttype=(amount < 0 and 'payment' or 'receipt'))
+ voucher_line_dict = False
+ if result['value']['line_ids']:
+ for line_dict in result['value']['line_ids']:
+ move_line = line_obj.browse(cr, uid, line_dict['move_line_id'], context)
+ if line.move_id.id == move_line.move_id.id:
+ voucher_line_dict = line_dict
+
+ if voucher_line_dict:
+ voucher_line_dict.update({'voucher_id':voucher_id})
+ voucher_line_obj.create(cr, uid, voucher_line_dict, context=context)
+
+ # reconcile_id = statement_reconcile_obj.create(cr, uid, {
+ # 'line_ids': [(6, 0, rec_id)]
+ # }, context=context)
+ #
+
+ mv = self.pool.get('account.move.line').browse(cr, uid, rec_id[0], context=context)
+ if mv.partner_id:
+ line['partner_id'] = mv.partner_id.id
+ if line['amount'] < 0 :
+ line['account_id'] = mv.partner_id.property_account_payable.id
+ else :
+ line['account_id'] = mv.partner_id.property_account_receivable.id
str_not1 = ''
if line.has_key('contry_name') and line.has_key('cntry_number'):
str_not1="Partner name:%s \n Partner Account Number:%s \n Communication:%s \n Value Date:%s \n Entry Date:%s \n"%(line["contry_name"], line["cntry_number"], line["free_comm"]+line['extra_note'], line["val_date"][0], line["entry_date"][0])
- id=bank_statement_line_obj.create(cr, uid, {
+ id = bank_statement_line_obj.create(cr, uid, {
'name':line['name'],
'date': line['date'],
'amount': line['amount'],
'partner_id':line['partner_id'] or 0,
'account_id':line['account_id'],
'statement_id': bk_st_id,
- 'reconcile_id': reconcile_id,
+ 'voucher_id': voucher_id,
'note':str_not1,
'ref':line['ref'],
})
- str_not= "\n \n Account Number: %s \n Account Holder Name: %s " %(statement["acc_number"], statement["acc_holder"])
+ str_not = "\n \n Account Number: %s \n Account Holder Name: %s " %(statement["acc_number"], statement["acc_holder"])
std_log += "\nStatement : %s , Date : %s, Starting Balance : %.2f , Ending Balance : %.2f \n"\
%(statement['name'], statement['date'], float(statement["balance_start"]), float(statement["balance_end_real"]))
bkst_list.append(bk_st_id)
@@ -267,7 +296,7 @@ class account_coda_import(osv.osv_memory):
'date': time.strftime("%Y-%m-%d"),
'user_id': uid,
})
- test=''
+ test = ''
test = str_log1 + std_log + err_log
self.write(cr, uid, ids, {'note': test}, context=context)
extraction = { 'statment_ids': bkst_list}
@@ -291,6 +320,7 @@ class account_coda_import(osv.osv_memory):
def action_open_window(self, cr, uid, data, context=None):
if not context:
cotext = {}
+
return {
'domain':"[('id','in',%s)]"%(context.get('statment_ids', False)),
'name': 'Statement',
@@ -299,7 +329,7 @@ class account_coda_import(osv.osv_memory):
'res_model': 'account.bank.statement',
'view_id': False,
'type': 'ir.actions.act_window',
- }
+ }
account_coda_import()
diff --git a/addons/account_coda/wizard/account_coda_import.xml b/addons/account_coda/wizard/account_coda_import.xml
index f344221c411..480e86d1b97 100644
--- a/addons/account_coda/wizard/account_coda_import.xml
+++ b/addons/account_coda/wizard/account_coda_import.xml
@@ -7,19 +7,20 @@
form
@@ -31,12 +35,46 @@
-
+
+ account_followup.stat.search
+ account_followup.stat
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Follow-ups Sent
account_followup.stat
form
tree,graph
+ {'search_default_followup_level':1,'search_default_fiscalyear':1}
+
diff --git a/addons/account_payment/__openerp__.py b/addons/account_payment/__openerp__.py
index 5114a576512..be0ca2a127b 100644
--- a/addons/account_payment/__openerp__.py
+++ b/addons/account_payment/__openerp__.py
@@ -33,7 +33,7 @@
* a basic mechanism to easily plug various automated payment.
""",
'author': 'OpenERP SA',
- 'depends': ['account'],
+ 'depends': ['account_voucher'],
'init_xml': [],
'update_xml': [
'security/account_payment_security.xml',
@@ -49,7 +49,7 @@
],
'demo_xml': ['account_payment_demo.xml'],
'test': [
- 'test/account_payment.yml'
+ 'test/account_payment.yml',
'test/account_payment_report.yml'
],
'installable': True,
diff --git a/addons/account_payment/wizard/account_payment_populate_statement.py b/addons/account_payment/wizard/account_payment_populate_statement.py
index 38d742119ad..0e7ea32e8d3 100644
--- a/addons/account_payment/wizard/account_payment_populate_statement.py
+++ b/addons/account_payment/wizard/account_payment_populate_statement.py
@@ -19,6 +19,7 @@
#
##############################################################################
+import time
from lxml import etree
from osv import osv, fields
@@ -53,7 +54,9 @@ class account_payment_populate_statement(osv.osv_memory):
statement_obj = self.pool.get('account.bank.statement')
statement_line_obj = self.pool.get('account.bank.statement.line')
currency_obj = self.pool.get('res.currency')
- statement_reconcile_obj = self.pool.get('account.bank.statement.reconcile')
+ voucher_obj = self.pool.get('account.voucher')
+ voucher_line_obj = self.pool.get('account.voucher.line')
+
if context is None:
context = {}
data = self.read(cr, uid, ids, [])[0]
@@ -68,11 +71,37 @@ class account_payment_populate_statement(osv.osv_memory):
ctx['date'] = line.ml_maturity_date # was value_date earlier,but this field exists no more now
amount = currency_obj.compute(cr, uid, line.currency.id,
statement.currency.id, line.amount_currency, context=ctx)
+ if line.partner_id:
+# line['partner_id'] = mv.partner_id.id
+ if amount < 0 :
+ account = line.partner_id.property_account_payable.id
+ else :
+ account = line.partner_id.property_account_receivable.id
if line.move_line_id:
- reconcile_id = statement_reconcile_obj.create(cr, uid, {
- 'line_ids': [(6, 0, [line.move_line_id.id])]
- }, context=context)
+ voucher_res = { 'type': 'payment' ,
+ 'name': line.name,
+ 'partner_id': line.partner_id.id,
+ 'journal_id': statement.journal_id.id,
+ 'account_id': account,
+ 'company_id': statement.company_id.id,
+ 'currency_id': statement.currency.id,
+ 'date': line.date or time.strftime('%Y-%m-%d'),
+ 'amount': abs(amount),
+ 'period_id': statement.period_id.id
+ }
+ voucher_id = voucher_obj.create(cr, uid, voucher_res, context=context)
+ result = voucher_obj.onchange_partner_id(cr, uid, [], partner_id=line.partner_id.id, journal_id=statement.journal_id.id, price=abs(amount), currency_id= statement.currency.id, ttype='payment')
+ voucher_line_dict = False
+ if result['value']['line_ids']:
+ for line_dict in result['value']['line_ids']:
+ move_line = self.pool.get('account.move.line').browse(cr, uid, line_dict['move_line_id'], context)
+ if line.move_line_id.move_id.id == move_line.move_id.id:
+ voucher_line_dict = line_dict
+ if voucher_line_dict:
+ voucher_line_dict.update({'voucher_id':voucher_id})
+ voucher_line_obj.create(cr, uid, voucher_line_dict, context=context)
+
st_line_id = statement_line_obj.create(cr, uid, {
'name': line.order_id.reference or '?',
'amount': - amount,
@@ -81,8 +110,9 @@ class account_payment_populate_statement(osv.osv_memory):
'account_id': line.move_line_id.account_id.id,
'statement_id': statement.id,
'ref': line.communication,
- 'reconcile_id': reconcile_id,
+ 'voucher_id': voucher_id,
}, context=context)
+
line_obj.write(cr, uid, [line.id], {'bank_statement_line_id': st_line_id})
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/base_calendar/base_calendar_view.xml b/addons/base_calendar/base_calendar_view.xml
index c3292452298..2a0ed14080f 100644
--- a/addons/base_calendar/base_calendar_view.xml
+++ b/addons/base_calendar/base_calendar_view.xml
@@ -122,7 +122,7 @@
separator="1" help="Delegated Invitations" />
-
+
diff --git a/addons/crm/crm.py b/addons/crm/crm.py
index c6f7759859c..edfe79540e6 100644
--- a/addons/crm/crm.py
+++ b/addons/crm/crm.py
@@ -179,7 +179,7 @@ class crm_case(object):
if not stage_record.section_id:
section_id = False # only select stages without section
- domain = [('object_id.model', '=', self._name), ('section_id', '=', section_id)]
+ domain = [('object_id.model', '=', self._name), '|', ('section_id', '=', section_id),('section_id','=',False)]
if 'force_domain' in context and context['force_domain']:
domain += context['force_domain']
sid = stage_obj.search(cr, uid, domain, context=context)
@@ -580,10 +580,8 @@ crm_case_section()
class crm_case_categ(osv.osv):
""" Category of Case """
-
_name = "crm.case.categ"
- _description = "Category of case"
-
+ _description = "Category of Case"
_columns = {
'name': fields.char('Name', size=64, required=True, translate=True),
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 0f81535e9be..fa04e70b92c 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -120,15 +120,15 @@ class crm_lead(crm_case, osv.osv):
# Lead fields
'categ_id': fields.many2one('crm.case.categ', 'Category', \
- domain="['|',('section_id','=',section_id),('section_id','=',False)]"),
+ domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.project.bug')]"),
'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
domain="['|',('section_id','=',section_id),('section_id','=',False)]"),
'channel_id': fields.many2one('res.partner.canal', 'Channel'),
'contact_name': fields.char('Contact Name', size=64),
'partner_name': fields.char("Partner Name", size=64),
- 'optin': fields.boolean('Opt-In'),
- 'optout': fields.boolean('Opt-Out'),
+ 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
+ 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
'type':fields.selection([
('lead','Lead'),
('opportunity','Opportunity'),
diff --git a/addons/crm/crm_lead_menu.xml b/addons/crm/crm_lead_menu.xml
index 0bb854fea17..65daf5d79d5 100644
--- a/addons/crm/crm_lead_menu.xml
+++ b/addons/crm/crm_lead_menu.xml
@@ -10,7 +10,7 @@
{"search_default_user_id":uid,'search_default_current':1}
- {'search_default_current':1, 'default_type': 'lead'}
+ {'search_default_current':1, 'default_type': 'lead', 'search_default_section_id':section_id}
A lead is a first, unqualified, contact with a new prospect. Use leads when you import a database of prospects or integrate your website's contact form with OpenERP. After having been qualified, the lead can be converted to a partner and a business opportunity for a further detailed tracking of the related activities.
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index 2df1369bbb1..47da5111692 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -32,20 +32,6 @@
id="menu_crm_lead_categ" name="Categories"
parent="base.menu_crm_config_lead" sequence="1"/>
-
-
- Lead Sources
- crm.case.resource.type
- form
-
- [('object_id.model', '=', 'crm.lead')]
- {'object_id':'crm.lead'}
-
-
-
@@ -70,7 +56,7 @@
+ domain="[('section_id','=',section_id), ('section_id','=',False), ('object_id.model', '=', 'crm.lead')]" />
-
-
+
+
+
-
-
-
+
+
-
diff --git a/addons/crm/crm_opportunity_data.xml b/addons/crm/crm_opportunity_data.xml
index 3ab446440f2..6726714fd04 100644
--- a/addons/crm/crm_opportunity_data.xml
+++ b/addons/crm/crm_opportunity_data.xml
@@ -2,37 +2,37 @@
- Existing Customer
+ Interrest in Computer
-
+
- Self Generated
+ Interrest in Accessories
- Employee
+ Need Services
- Partner
+ Need Information
- Campaign
+ Need a Website Design
- Website
+ Potential Reseller
- Word of mouth
+ Need Consulting
diff --git a/addons/crm/crm_opportunity_menu.xml b/addons/crm/crm_opportunity_menu.xml
index 0bfb5f35efc..d2d2a299edb 100644
--- a/addons/crm/crm_opportunity_menu.xml
+++ b/addons/crm/crm_opportunity_menu.xml
@@ -28,7 +28,7 @@
crm.lead
tree,form,graph,calendar
[('type','=','opportunity')]
- {'search_default_user_id':uid,'search_default_current':1, 'default_type': 'opportunity'}
+ {'search_default_user_id':uid,'search_default_current':1, 'search_default_section_id':section_id,'default_type': 'opportunity'}
Opportunities allows you to track your best deals pipeline: history of
diff --git a/addons/crm/crm_opportunity_view.xml b/addons/crm/crm_opportunity_view.xml
index c2713b91f89..4536497d6b7 100644
--- a/addons/crm/crm_opportunity_view.xml
+++ b/addons/crm/crm_opportunity_view.xml
@@ -15,7 +15,7 @@
+ domain="['|',('section_id','=',section_id),('section_id','=',False),('object_id.model', '=', 'crm.lead')]" />
@@ -62,7 +62,7 @@
-
@@ -118,7 +118,7 @@
-
+
@@ -211,7 +211,7 @@
crm.lead
tree
-
+
@@ -337,7 +337,7 @@
+ domain="[]" context="{'group_by':'type_id'}" groups="base.group_extended"/>
diff --git a/addons/crm/crm_phonecall.py b/addons/crm/crm_phonecall.py
index afa153fb662..96dde7139df 100644
--- a/addons/crm/crm_phonecall.py
+++ b/addons/crm/crm_phonecall.py
@@ -61,14 +61,11 @@ class crm_phonecall(crm_case, osv.osv):
\nWhen the case is over, the state is set to \'Done\'.\
\nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
'email_from': fields.char('Email', size=128, help="These people will receive email."),
- 'stage_id': fields.many2one('crm.case.stage', 'Stage', \
- domain="[('section_id','=',section_id),\
- ('object_id.model', '=', 'crm.phonecall')]"),
'date_open': fields.datetime('Opened', readonly=True),
# phonecall fields
'duration': fields.float('Duration', help="Duration in Minutes"),
'categ_id': fields.many2one('crm.case.categ', 'Category', \
- domain="[('section_id','=',section_id),\
+ domain="['|',('section_id','=',section_id),('section_id','=',False),\
('object_id.model', '=', 'crm.phonecall')]"),
'partner_phone': fields.char('Phone', size=32),
'partner_contact': fields.related('partner_address_id', 'name', \
diff --git a/addons/crm/crm_phonecall_menu.xml b/addons/crm/crm_phonecall_menu.xml
index 8585ddc7fe4..44f1bcb219f 100644
--- a/addons/crm/crm_phonecall_menu.xml
+++ b/addons/crm/crm_phonecall_menu.xml
@@ -32,7 +32,7 @@
form
tree,calendar
- {"search_default_user_id":uid}
+ {"search_default_user_id":uid, 'search_default_section_id':section_id}
@@ -106,7 +106,7 @@
tree,calendar
-
+
The Outbound menu open a search view in editable mode, so you can easily modify or plan a new call. Actions buttons allows you to to make you call status evolved for a better follow up of your planned calls. During calls, convert your lead into an opportunity, plan a meeting or canceled it.
diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml
index 0832c0a16ee..4f07b549576 100644
--- a/addons/crm/crm_phonecall_view.xml
+++ b/addons/crm/crm_phonecall_view.xml
@@ -71,7 +71,7 @@
attrs="{'invisible':[('opportunity_id','!=',False)]}" />
@@ -150,7 +150,7 @@
attrs="{'invisible':[('opportunity_id','!=',False)]}" />
diff --git a/addons/crm/report/crm_lead_report.py b/addons/crm/report/crm_lead_report.py
index d4b32bf0434..cd8346de229 100644
--- a/addons/crm/report/crm_lead_report.py
+++ b/addons/crm/report/crm_lead_report.py
@@ -100,9 +100,9 @@ class crm_lead_report(osv.osv):
'planned_revenue': fields.float('Planned Revenue',digits=(16,2),readonly=True),
'probable_revenue': fields.float('Probable Revenue', digits=(16,2),readonly=True),
'categ_id': fields.many2one('crm.case.categ', 'Category',\
- domain="[('section_id','=',section_id)]" , readonly=True),
+ domain="['|',('section_id','=',False),('section_id','=',section_id)]" , readonly=True),
'stage_id': fields.many2one ('crm.case.stage', 'Stage', \
- domain="[('section_id','=',section_id),\
+ domain="['|',('section_id','=',False),('section_id','=',section_id),\
('object_id.model', '=', 'crm.lead')]", readonly=True),
'partner_id': fields.many2one('res.partner', 'Partner' , readonly=True),
'opening_date': fields.date('Opening Date', readonly=True),
diff --git a/addons/crm/report/crm_lead_report_view.xml b/addons/crm/report/crm_lead_report_view.xml
index 7b31b0bab2c..c37c7bcec57 100644
--- a/addons/crm/report/crm_lead_report_view.xml
+++ b/addons/crm/report/crm_lead_report_view.xml
@@ -99,13 +99,13 @@
-
-
+
@@ -132,7 +132,9 @@
context="{'group_by':'section_id'}" />
-
+
@@ -146,10 +148,6 @@
-
-
-
@@ -92,14 +91,14 @@
string="Todo"
domain="[('state','in',('draft','open'))]"
help="Phone calls which are in draft and open state"/>
-
+
-
+
-
-
+
@@ -130,17 +129,14 @@
-
-
-
+
-
diff --git a/addons/crm/wizard/crm_opportunity_to_phonecall.py b/addons/crm/wizard/crm_opportunity_to_phonecall.py
index fdf5cfc1f37..bd2aaca0607 100644
--- a/addons/crm/wizard/crm_opportunity_to_phonecall.py
+++ b/addons/crm/wizard/crm_opportunity_to_phonecall.py
@@ -34,7 +34,7 @@ class crm_opportunity2phonecall(osv.osv_memory):
'date': fields.datetime('Date' , required=True),
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
'categ_id': fields.many2one('crm.case.categ', 'Category', required=True, \
- domain="[('section_id','=',section_id),\
+ domain="['|',('section_id','=',False),('section_id','=',section_id),\
('object_id.model', '=', 'crm.phonecall')]"),
}
diff --git a/addons/crm/wizard/crm_phonecall_to_phonecall.py b/addons/crm/wizard/crm_phonecall_to_phonecall.py
index f288fd2d5b7..aaba8e082c5 100644
--- a/addons/crm/wizard/crm_phonecall_to_phonecall.py
+++ b/addons/crm/wizard/crm_phonecall_to_phonecall.py
@@ -103,7 +103,7 @@ class crm_phonecall2phonecall(osv.osv_memory):
'name' : fields.char('Call summary', size=64, required=True, select=1),
'user_id' : fields.many2one('res.users',"Assign To"),
'categ_id': fields.many2one('crm.case.categ', 'Category', required=True, \
- domain="[('section_id','=',section_id),\
+ domain="['|',('section_id','=',False),('section_id','=',section_id),\
('object_id.model', '=', 'crm.phonecall')]"),
'date': fields.datetime('Date', required=True),
'section_id':fields.many2one('crm.case.section','Sales Team'),
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index de77c842aa6..d6c6603b829 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -54,9 +54,7 @@ class crm_claim(crm.crm_case, osv.osv):
domain="[('section_id','=',section_id),\
('object_id.model', '=', 'crm.claim')]"),
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
- 'type_id': fields.many2one('crm.case.resource.type', 'Claim Type', \
- domain="[('section_id','=',section_id),\
- ('object_id.model', '=', 'crm.claim')]"),
+ 'type_action': fields.selection([('correction','Corrective Action'),('prevention','Preventive Action')], 'Action Type'),
'user_id': fields.many2one('res.users', 'Responsible'),
'section_id': fields.many2one('crm.case.section', 'Sales Team', \
select=True, help="Sales team to which Case belongs to."\
diff --git a/addons/crm_claim/crm_claim_data.xml b/addons/crm_claim/crm_claim_data.xml
index a176b3aa8e0..cd8db8499a5 100644
--- a/addons/crm_claim/crm_claim_data.xml
+++ b/addons/crm_claim/crm_claim_data.xml
@@ -1,78 +1,76 @@
-
+
-
-
-
- Factual Claims
-
-
-
-
-
- Value Claims
-
-
-
-
-
- Policy Claims
-
-
-
-
-
-
-
- Corrective
-
-
-
-
-
- Preventive
-
-
-
-
-
-
-
- Accepted as Claim
-
-
-
-
-
- Fixed
-
-
-
-
-
- Won't fix
-
-
-
-
-
- Invalid
-
-
-
-
-
- Awaiting Response
-
-
-
-
-
+
+
+
+ Factual Claims
+
+
+
+
+
+ Value Claims
+
+
+
+
+
+ Policy Claims
+
+
+
+
+
+
+
+ Corrective
+
+
+
+
+ Preventive
+
+
+
+
+
+
+ Accepted as Claim
+
+
+
+
+
+ Fixed
+
+
+
+
+
+ Won't fix
+
+
+
+
+
+ Invalid
+
+
+
+
+
+ Awaiting Response
+
+
+
+
+
diff --git a/addons/crm_claim/crm_claim_demo.xml b/addons/crm_claim/crm_claim_demo.xml
index db21223656e..0a603cb8f5a 100644
--- a/addons/crm_claim/crm_claim_demo.xml
+++ b/addons/crm_claim/crm_claim_demo.xml
@@ -1,129 +1,122 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml
index a341915bc39..e7b8d65e711 100644
--- a/addons/crm_claim/crm_claim_view.xml
+++ b/addons/crm_claim/crm_claim_view.xml
@@ -42,7 +42,7 @@
crm.claim
tree
-
+
@@ -117,8 +117,7 @@
-
+
diff --git a/addons/crm_claim/report/crm_claim_report.py b/addons/crm_claim/report/crm_claim_report.py
index 9005f4dff57..16bc16ecd22 100644
--- a/addons/crm_claim/report/crm_claim_report.py
+++ b/addons/crm_claim/report/crm_claim_report.py
@@ -70,9 +70,7 @@ class crm_claim_report(osv.osv):
'partner_id': fields.many2one('res.partner', 'Partner', readonly=True),
'company_id': fields.many2one('res.company', 'Company', readonly=True),
'priority': fields.selection(AVAILABLE_PRIORITIES, 'Priority'),
- 'type_id': fields.many2one('crm.case.resource.type', 'Claim Type',\
- domain="[('section_id','=',section_id),\
- ('object_id.model', '=', 'crm.claim')]"),
+ 'type_action': fields.selection([('correction','Corrective Action'),('prevention','Preventive Action')], 'Action Type'),
'date_closed': fields.date('Closed', readonly=True),
'delay_expected': fields.float('Overpassed Deadline',digits=(16,2),readonly=True, group_operator="avg"),
}
@@ -101,7 +99,7 @@ class crm_claim_report(osv.osv):
c.categ_id,
count(*) as nbr,
c.priority as priority,
- c.type_id as type_id,
+ c.type_action as type_action,
date_trunc('day',c.create_date) as create_date,
avg(extract('epoch' from (c.date_closed-c.create_date)))/(3600*24) as delay_close,
extract('epoch' from (c.date_deadline - c.date_closed))/(3600*24) as delay_expected
@@ -110,7 +108,7 @@ class crm_claim_report(osv.osv):
group by to_char(c.date, 'YYYY'), to_char(c.date, 'MM'),to_char(c.date, 'YYYY-MM-DD'),\
c.state, c.user_id,c.section_id, c.stage_id,\
c.categ_id,c.partner_id,c.company_id,c.create_date,
- c.priority,c.type_id,c.date_deadline,c.date_closed
+ c.priority,c.type_action,c.date_deadline,c.date_closed
)""")
crm_claim_report()
diff --git a/addons/crm_claim/report/crm_claim_report_view.xml b/addons/crm_claim/report/crm_claim_report_view.xml
index a9d7283dc74..5eeecf3d3ad 100644
--- a/addons/crm_claim/report/crm_claim_report_view.xml
+++ b/addons/crm_claim/report/crm_claim_report_view.xml
@@ -10,21 +10,21 @@
tree
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -36,11 +36,11 @@
crm.claim.report
form
-
-
+
-
+
+
+
@@ -130,7 +131,7 @@
-
diff --git a/addons/hr_attendance/hr_attendance_view.xml b/addons/hr_attendance/hr_attendance_view.xml
index 9768d19c860..9339bf72939 100644
--- a/addons/hr_attendance/hr_attendance_view.xml
+++ b/addons/hr_attendance/hr_attendance_view.xml
@@ -78,7 +78,10 @@
-
+
+
+
+
hr.action.reason.form
diff --git a/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml b/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml
index 21095e9f0d4..cf86710ac93 100644
--- a/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml
+++ b/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml
@@ -47,7 +47,7 @@
+ parent="menu_hr_attendance" sequence="4" groups="base.group_hr_manager,base.group_hr_user,base.group_hr_attendance"/>
hr.sign.in.out.ask.form
diff --git a/addons/hr_payroll/report/payslip.rml b/addons/hr_payroll/report/payslip.rml
index 7dae0a9b06a..58b2fd81275 100755
--- a/addons/hr_payroll/report/payslip.rml
+++ b/addons/hr_payroll/report/payslip.rml
@@ -333,7 +333,7 @@
Identification No
- [[ o.employee_id.pan_no or '' ]]
+ [[ o.employee_id.otherid or '' ]]
diff --git a/addons/hr_payroll/report/report_emp_salary_structure.rml b/addons/hr_payroll/report/report_emp_salary_structure.rml
index ee564052af3..6bfc528e4e0 100644
--- a/addons/hr_payroll/report/report_emp_salary_structure.rml
+++ b/addons/hr_payroll/report/report_emp_salary_structure.rml
@@ -290,10 +290,10 @@
[[ employee.name ]]
- PAN No.
+ Other Id
- [[ employee.pan_no or '' ]]
+ [[ employee.otherid or '' ]]
Address
diff --git a/addons/hr_payroll/report/report_employees_detail.py b/addons/hr_payroll/report/report_employees_detail.py
index ca06af969e7..004dae84531 100644
--- a/addons/hr_payroll/report/report_employees_detail.py
+++ b/addons/hr_payroll/report/report_employees_detail.py
@@ -229,7 +229,7 @@ class employees_salary_report(rml_parse.rml_parse):
def get_monthly_total(self):
return self.month_total_list
-report_sxw.report_sxw('report.employees.salary', 'hr.payslip', 'hr_payroll/report/report_employees_detail.rml', parser=employees_salary_report)
+report_sxw.report_sxw('report.employees.salary', 'hr.payslip', 'hr_payroll/report/report_employees_detail.rml', parser=employees_salary_report,header=3)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_payroll/report/report_employees_detail.rml b/addons/hr_payroll/report/report_employees_detail.rml
index 3b032af3409..1988d932ce2 100644
--- a/addons/hr_payroll/report/report_employees_detail.rml
+++ b/addons/hr_payroll/report/report_employees_detail.rml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -10,6 +10,10 @@
+
+
+
+
@@ -111,6 +115,10 @@
+
+
+
+
@@ -129,6 +137,10 @@
+
+
+
+
@@ -147,6 +159,10 @@
+
+
+
+
@@ -172,6 +188,8 @@
+
+
@@ -229,12 +247,21 @@
[[ repeatIn(get_employee(data['form']), 'o') ]] [[ get_employee_detail(o) ]]
- Employees Salary Details
- For [[ get_fiscalyear(data['form']['fiscalyear_id']) ]]
-
+
-
+
+
+
+ Employees Salary Details
+ For [[ get_fiscalyear(data['form']['fiscalyear_id']) ]]
+
+
+
+
+
+
+
Employee Code
@@ -256,7 +283,7 @@
-
+
Employee Name
@@ -265,10 +292,10 @@
[[ o.name ]]
- PAN No.
+ Other Id
- [[ o.pan_no or '' ]]
+ [[ o.otherid or '' ]]
Address
@@ -278,7 +305,7 @@
-
+
Designation
@@ -305,7 +332,7 @@
[[ repeatIn(get_periods(data['form']),'m') ]]
-
+
Title
@@ -358,10 +385,16 @@
- Allowances with Basic:
+
+
+
+ Allowances with Basic:
+
+
+
[[ repeatIn(get_allow(),'e1') ]]
-
+
[[ e1[0] ]]
@@ -411,10 +444,16 @@
- Deductions:
+
+
+
+ Deductions:
+
+
+
[[ repeatIn(get_deduct(),'e2') ]]
-
+
[[ e2[0] ]]
@@ -464,10 +503,16 @@
- Others:
+
+
+
+ Others:
+
+
+
[[ repeatIn(get_other(),'e3') ]]
-
+
[[ e3[0] ]]
@@ -524,7 +569,7 @@
-
+
[[ get_monthly_total()[0] ]]
@@ -581,4 +626,4 @@
-
\ No newline at end of file
+
diff --git a/addons/hr_payroll/report/report_year_report.rml b/addons/hr_payroll/report/report_year_report.rml
index 58448de5704..4c32436955a 100755
--- a/addons/hr_payroll/report/report_year_report.rml
+++ b/addons/hr_payroll/report/report_year_report.rml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -124,6 +124,9 @@
+
+
+
Yearly Salary Details
For [[ get_fiscalyear(data['form']['fiscalyear_id']) ]]
@@ -131,7 +134,7 @@
[[ repeatIn(get_periods(data['form']),'m') ]]
-
+
Name
@@ -182,7 +185,7 @@
[[ repeatIn(get_employee(data['form']),'e') ]]
-
+
[[ e[0] ]]
@@ -241,7 +244,7 @@
[[ repeatIn(get_months_tol(),'t') ]]
-
+
Total([[company.currency_id.code]])
diff --git a/addons/hr_payroll/report/report_year_salary.py b/addons/hr_payroll/report/report_year_salary.py
index 66cf05db601..b0f02bc077e 100755
--- a/addons/hr_payroll/report/report_year_salary.py
+++ b/addons/hr_payroll/report/report_year_salary.py
@@ -9,7 +9,7 @@ import mx.DateTime
from mx.DateTime import RelativeDateTime, now, DateTime, localtime
class year_salary_report(rml_parse.rml_parse):
-
+
def __init__(self, cr, uid, name, context):
super(year_salary_report, self).__init__(cr, uid, name, context)
self.localcontext.update({
@@ -20,21 +20,21 @@ class year_salary_report(rml_parse.rml_parse):
'get_fiscalyear' : self.get_fiscalyear,
'get_total' : self.get_total,
})
-
+
self.mnths =[]
self.mnths_tol = []
self.curr_fiscal_year_name=''
self.total=0.0
-
+
def get_periods(self,form):
fiscalyear = pooler.get_pool(self.cr.dbname).get('account.fiscalyear')
curr_fiscalyear_id = form['fiscalyear_id']
curr_fiscalyear = fiscalyear.read(self.cr,self.uid,[form['fiscalyear_id']],['date_start','date_stop'])[0]
-
+
# Get start year-month-date and end year-month-date
- fy = int(curr_fiscalyear['date_start'][0:4])
+ fy = int(curr_fiscalyear['date_start'][0:4])
ly = int(curr_fiscalyear['date_stop'][0:4])
-
+
fm = int(curr_fiscalyear['date_start'][5:7])
lm = int(curr_fiscalyear['date_stop'][5:7])
no_months = (ly-fy)*12+lm-fm + 1
@@ -46,7 +46,7 @@ class year_salary_report(rml_parse.rml_parse):
for count in range(0,no_months):
m = datetime.date(cy, cm, 1).strftime('%b')
mnth_name.append(m)
- self.mnths.append(str(cm)+'-'+str(cy))
+ self.mnths.append(str(cm)+'-'+str(cy))
if cm == 12:
cm = 0
cy = ly
@@ -57,13 +57,13 @@ class year_salary_report(rml_parse.rml_parse):
fiscalyear_obj = pooler.get_pool(self.cr.dbname).get('account.fiscalyear')
return fiscalyear_obj.read(self.cr,self.uid,[fiscalyear_id],['name'])[0]['name']
-
+
def get_employee(self,form):
- ls1=[]
+ ls1=[]
ls = []
periods = []
- tol_mnths=['Total',0,0,0,0,0,0,0,0,0,0,0,0]
- emp = pooler.get_pool(self.cr.dbname).get('hr.employee')
+ tol_mnths=['Total',0,0,0,0,0,0,0,0,0,0,0,0]
+ emp = pooler.get_pool(self.cr.dbname).get('hr.employee')
emp_ids = form['employee_ids']
empll = emp.browse(self.cr,self.uid, emp_ids)
fiscalyear_obj = pooler.get_pool(self.cr.dbname).get('account.fiscalyear').browse(self.cr, self.uid, form['fiscalyear_id'])
@@ -72,7 +72,7 @@ class year_salary_report(rml_parse.rml_parse):
periods.append(period.id)
periods_ids = ','.join(map(str, periods))
cnt = 1
- for emp_id in empll:
+ for emp_id in empll:
ls1.append(emp_id.name)
tol = 0.0
for mnth in self.mnths:
@@ -84,30 +84,30 @@ class year_salary_report(rml_parse.rml_parse):
try:
ls1.append(sal[0][0])
except:
- ls1.append(0)
- try:
+ ls1.append(0)
+ try:
tol += sal[0][0]
tol_mnths[cnt] = tol_mnths[cnt] + sal[0][0]
except:
- tol += 0
- cnt = cnt + 1
- cnt = 1
+ tol += 0
+ cnt = cnt + 1
+ cnt = 1
ls1.append(tol)
ls.append(ls1)
ls1 = []
self.mnths_tol.append(tol_mnths)
return ls
-
+
def get_months_tol(self):
return self.mnths_tol
-
+
def get_total(self):
for item in self.mnths_tol:
for count in range(1,len(item)):
self.total += item[count]
return self.total
-
-report_sxw.report_sxw('report.year.salary', 'hr.payslip', 'hr_payroll/report/report_year_report.rml', parser=year_salary_report)
-
-
-
+
+report_sxw.report_sxw('report.year.salary', 'hr.payslip', 'hr_payroll/report/report_year_report.rml', parser=year_salary_report,header=3)
+
+
+
diff --git a/addons/hr_payroll/report/rml_parse.py b/addons/hr_payroll/report/rml_parse.py
index 550c77c696d..2e3ca9dc019 100755
--- a/addons/hr_payroll/report/rml_parse.py
+++ b/addons/hr_payroll/report/rml_parse.py
@@ -28,6 +28,7 @@ import tools
import pooler
import re
import sys
+from lxml import etree
class rml_parse(report_sxw.rml_parse):
@@ -153,7 +154,18 @@ class rml_parse(report_sxw.rml_parse):
def _add_header(self, node, header=1):
if header==2:
rml_head = self.rml_header2
+ elif header==3:
+ rml_head = self.rml_header3
else:
rml_head = self.rml_header
rml_head = rml_head.replace('',''' [[company.logo]] ''')
+ rml_dom = node
+ head_dom = etree.XML(rml_head)
+ for tag in head_dom:
+ found = rml_dom.find('.//'+tag.tag)
+ if found is not None and len(found):
+ if tag.get('position'):
+ found.append(tag)
+ else :
+ found.getparent().replace(found,tag)
return True
diff --git a/addons/hr_recruitment/hr_recruitment_menu.xml b/addons/hr_recruitment/hr_recruitment_menu.xml
index e49afdee833..21f9074545a 100644
--- a/addons/hr_recruitment/hr_recruitment_menu.xml
+++ b/addons/hr_recruitment/hr_recruitment_menu.xml
@@ -1,47 +1,50 @@
-
+
- ######################## JOB OPPORTUNITIES (menu) ###########################
-
- Applicants
- hr.applicant
- tree,form,graph,calendar
-
- {"search_default_user_id":uid, 'search_default_current': 1}
-
-
+ ######################## JOB OPPORTUNITIES (menu) ###########################
+
+ Applicants
+ hr.applicant
+ tree,form,graph,calendar
+
+ {"search_default_user_id":uid, 'search_default_current': 1}
+
+
-
-
- tree
-
-
-
+
+
+ tree
+
+
+
-
-
- form
-
-
-
+
+
+ form
+
+
+
-
-
- graph
-
-
-
+
+
+ graph
+
+
+
-
+
-
+
-
+
-
+
diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml
index 149636de112..3da942873b6 100644
--- a/addons/hr_recruitment/hr_recruitment_view.xml
+++ b/addons/hr_recruitment/hr_recruitment_view.xml
@@ -61,8 +61,6 @@
-
-
@@ -77,8 +75,8 @@
-
-
+
+
@@ -87,11 +85,11 @@
-
-
+
+
-
-
+
+
diff --git a/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml b/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml
index 18e5e4f11df..73c01c9c54f 100644
--- a/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml
+++ b/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml
@@ -1,47 +1,47 @@
-
+
hr.sign.in.project.message.form
hr.sign.in.project
form
-
+
hr.sign.in.project.form
hr.sign.in.project
form
-
+
Sign in / Sign out by project
hr.sign.in.project
form
@@ -52,41 +52,41 @@
+ id="menu_hr_timesheet_sign_in"
+ groups="base.group_extended"
+ parent="hr_attendance.menu_hr_attendance"
+ sequence="5" />
-
+
hr.sign.out.project.form
hr.sign.out.project
form
-
+
Sign in / Sign out by project
hr.sign.out.project
form
diff --git a/addons/l10n_ch/__openerp__.py b/addons/l10n_ch/__openerp__.py
index 1af2e70ca59..a9315abdf55 100644
--- a/addons/l10n_ch/__openerp__.py
+++ b/addons/l10n_ch/__openerp__.py
@@ -75,6 +75,7 @@ TODO :
"base_vat",
"base_iban",
"account_payment",
+ "account_voucher",
],
"init_xml" : [
"dta_data.xml",
diff --git a/addons/l10n_ch/report/bvr_invoice_report.rml b/addons/l10n_ch/report/bvr_invoice_report.rml
index 5507915c0af..e00c061522a 100755
--- a/addons/l10n_ch/report/bvr_invoice_report.rml
+++ b/addons/l10n_ch/report/bvr_invoice_report.rml
@@ -250,7 +250,7 @@
- [[ note or removeParentNode('blockTable') ]]
+ [[ l.note or removeParentNode('blockTable') ]]
[[ l.note ]]
diff --git a/addons/l10n_ch/test/l10n_ch_report.yml b/addons/l10n_ch/test/l10n_ch_report.yml
index 49c058a4bc8..a8a976bc424 100644
--- a/addons/l10n_ch/test/l10n_ch_report.yml
+++ b/addons/l10n_ch/test/l10n_ch_report.yml
@@ -3,6 +3,8 @@
-
!python {model: account.invoice}: |
import netsvc, tools, os
+ run_str = 'cp ' + tools.config['addons_path'] + '/l10n_ch/report/ocrbb.ttf' + ' ' + '/tmp/ocrbb.ttf'
+ os.system(run_str)
(data, format) = netsvc.LocalService('report.l10n_ch.bvr').create(cr, uid, [ref('account.test_invoice_1')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'l10ch-bvr_report.'+format), 'wb+').write(data)
diff --git a/addons/l10n_ch/wizard/bvr_import.py b/addons/l10n_ch/wizard/bvr_import.py
index 50eb6864381..611b5720a96 100644
--- a/addons/l10n_ch/wizard/bvr_import.py
+++ b/addons/l10n_ch/wizard/bvr_import.py
@@ -73,7 +73,9 @@ def _import(obj, cursor, user, data, context=None):
pool = pooler.get_pool(cursor.dbname)
statement_line_obj = pool.get('account.bank.statement.line')
- statement_reconcile_obj = pool.get('account.bank.statement.reconcile')
+# statement_reconcile_obj = pool.get('account.bank.statement.reconcile')
+ voucher_obj = self.pool.get('account.voucher')
+ voucher_line_obj = self.pool.get('account.voucher.line')
move_line_obj = pool.get('account.move.line')
property_obj = pool.get('ir.property')
model_fields_obj = pool.get('ir.model.fields')
@@ -200,9 +202,32 @@ def _import(obj, cursor, user, data, context=None):
values['partner_id'] = partner_id
if line2reconcile:
- values['reconcile_id'] = statement_reconcile_obj.create(cursor, user, {
- 'line_ids': [(6, 0, [line2reconcile])],
- }, context=context)
+ voucher_res = { 'type': 'payment' ,
+ 'name': line.name,
+ 'partner_id': line.partner_id.id,
+ 'journal_id': statement.journal_id.id,
+ 'account_id': account,
+ 'company_id': statement.company_id.id,
+ 'currency_id': statement.currency.id,
+ 'date': line.date or time.strftime('%Y-%m-%d'),
+ 'amount': abs(amount),
+ 'period_id': statement.period_id.id
+ }
+ voucher_id = voucher_obj.create(cr, uid, voucher_res, context=context)
+ result = voucher_obj.onchange_partner_id(cr, uid, [], partner_id=line.partner_id.id, journal_id=statement.journal_id.id, price=abs(amount), currency_id= statement.currency.id, ttype='payment')
+ voucher_line_dict = False
+ if result['value']['line_ids']:
+ for line_dict in result['value']['line_ids']:
+ move_line = self.pool.get('account.move.line').browse(cr, uid, line_dict['move_line_id'], context)
+ if line.move_line_id.move_id.id == move_line.move_id.id:
+ voucher_line_dict = line_dict
+ if voucher_line_dict:
+ voucher_line_dict.update({'voucher_id':voucher_id})
+ voucher_line_obj.create(cr, uid, voucher_line_dict, context=context)
+ values['voucher_id'] = voucher_id
+# values['reconcile_id'] = statement_reconcile_obj.create(cursor, user, {
+# 'line_ids': [(6, 0, [line2reconcile])],
+# }, context=context)
statement_line_obj.create(cursor, user, values, context=context)
attachment_obj.create(cursor, user, {
diff --git a/addons/l10n_ch/wizard/bvr_import_view.xml b/addons/l10n_ch/wizard/bvr_import_view.xml
index 7ac2e15c7f0..22061ebcfc9 100644
--- a/addons/l10n_ch/wizard/bvr_import_view.xml
+++ b/addons/l10n_ch/wizard/bvr_import_view.xml
@@ -15,7 +15,7 @@
-
+
@@ -29,8 +29,8 @@
form
new
-
-
+
+
account.bank.statement.form.inherit
account.bank.statement
@@ -38,10 +38,10 @@
-
+
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/addons/mrp_operations/mrp_operations.py b/addons/mrp_operations/mrp_operations.py
index 968650c6f7b..919f65903ae 100644
--- a/addons/mrp_operations/mrp_operations.py
+++ b/addons/mrp_operations/mrp_operations.py
@@ -39,6 +39,15 @@ class stock_move(osv.osv):
_columns = {
'move_dest_id_lines': fields.one2many('stock.move','move_dest_id', 'Children Moves')
}
+
+ def copy(self, cr, uid, id, default=None, context={}):
+ if default is None:
+ default = {}
+ default.update({
+ 'move_dest_id_lines': [],
+ })
+ return super(stock_move, self).copy(cr, uid, id, default, context)
+
stock_move()
class mrp_production_workcenter_line(osv.osv):
diff --git a/addons/point_of_sale/test/pos_report.yml b/addons/point_of_sale/test/pos_report.yml
index 1747fb60305..aeb99e4df57 100644
--- a/addons/point_of_sale/test/pos_report.yml
+++ b/addons/point_of_sale/test/pos_report.yml
@@ -1,51 +1,51 @@
-
- In order to test the PDF reports defined on a Point Of Sale, we will print a POS Detail Summary Report
--
+ In order to test the PDF reports defined on a Point Of Sale,we will print a POS Detail Summary Report
+-
!python {model: pos.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.pos.details_summary').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-details_summary report'+format), 'wb+').write(data)
-
+
-
In order to test the PDF reports defined on a Point Of Sale, we will print a POS Invoice Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.pos.invoice').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-invoice report'+format), 'wb+').write(data)
-
+
-
In order to test the PDF reports defined on a Point Of Sale, we will print a POS Lines Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.pos.lines').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-lines report'+format), 'wb+').write(data)
-
+
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Receipt Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.pos.receipt').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-receipt report'+format), 'wb+').write(data)
-
+
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Receipt With Reimbursement Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.pos.receipt.with.remboursment').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-receipt with reimbursement report'+format), 'wb+').write(data)
-
+
-
In order to test the PDF reports defined on a point of sale, we will print a POS Receipt Without Reimbursement Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.pos.receipt.without.remboursment').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
@@ -53,7 +53,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-receipt without reimbursement report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point Of Sale, we will print a POS Payment Report Report
--
+-
!python {model: ir.ui.menu}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.pos.payment.report.date').create(cr, uid, [1], {'model':'ir.ui.menu', 'form':{'date_start': time.strftime('%Y-%m-%d'), 'date_end': time.strftime('%Y-%m-%d'), 'user_id': [ref('base.user_root')] }}, {})
@@ -61,7 +61,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale- pos sales lines report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point Of Sale, we will print a POS Details Report
--
+-
!python {model: ir.ui.menu}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.pos.details').create(cr, uid, [], {'model':'ir.ui.menu', 'form':{'date_start': time.strftime('%Y-%m-%d'), 'date_end': time.strftime('%Y-%m-%d') }}, {})
@@ -69,7 +69,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale- pos details report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point Of Sale, we will print a Account Statement Report
--
+-
!python {model: account.bank.statement}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.account.statement').create(cr, uid, [ref('point_of_sale.account_bank_statement_st0')], {}, {})
@@ -77,7 +77,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-account_statement_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point Of Sale, we will print a POS User Product Report
--
+-
!python {model: account.bank.statement}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.pos.user.product').create(cr, uid, [ref('point_of_sale.account_bank_statement_st0')], {}, {})
@@ -85,7 +85,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-user_product_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point Of Sale, we will print a POS All Close Cashbox Of the Day Report
--
+-
!python {model: account.bank.statement}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.all.closed.cashbox.of.the.day').create(cr, uid, [ref('point_of_sale.account_bank_statement_st0')], {}, {})
@@ -93,7 +93,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-all_closed_cash_box_of_the_day_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Sales User Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os, time
data_dict = {'model':'ir.ui.menu', 'form':{'date_start': time.strftime('%Y-%m-%d'), 'date_end': time.strftime('%Y-%m-%d'),'user_id':[ref('base.user_root')] }}
@@ -102,7 +102,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-sales_user_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Sales User Today Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os, time
data_dict = {'model':'ir.ui.menu', 'form':{'user_id':[ref('base.user_root')] }}
@@ -111,7 +111,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-sales_user_today_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Payment Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.pos.payment.report').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
@@ -119,7 +119,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-payment_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Payment User Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os, time
data_dict = {'model':'ir.ui.menu', 'form':{'user_id':[ref('base.user_root'),ref('base.user_demo')] }}
@@ -128,7 +128,7 @@
file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-payment_user_report'+format), 'wb+').write(data)
-
In order to test the PDF reports defined on a Point of Sale, we will print a POS Today's Sales of Current User Report
--
+-
!python {model: pos.order}: |
import netsvc, tools, os, time
(data, format) = netsvc.LocalService('report.pos.sales.user.today.current.user').create(cr, uid, [ref('point_of_sale.order_pos1'), ref('point_of_sale.order_pos2')], {}, {})
diff --git a/addons/product/pricelist_view.xml b/addons/product/pricelist_view.xml
index 4d049d44843..589dfb5f290 100644
--- a/addons/product/pricelist_view.xml
+++ b/addons/product/pricelist_view.xml
@@ -4,7 +4,7 @@
-
+
product.pricelist.version.form
diff --git a/addons/project_issue/i18n/en_US.po b/addons/project_issue/i18n/en_US.po
index e015c70de7c..1f4a96616e2 100644
--- a/addons/project_issue/i18n/en_US.po
+++ b/addons/project_issue/i18n/en_US.po
@@ -254,9 +254,9 @@ msgstr "Send New Email"
#. module: project_issue
#: view:project.issue:0
-#: field:project.issue,type_id:0
+#: field:project.issue,version_id:0
#: view:project.issue.report:0
-#: field:project.issue.report,type_id:0
+#: field:project.issue.report,version_id:0
msgid "Version"
msgstr "Version"
diff --git a/addons/project_issue/i18n/fr.po b/addons/project_issue/i18n/fr.po
index 218c37a2cbf..316978046f5 100644
--- a/addons/project_issue/i18n/fr.po
+++ b/addons/project_issue/i18n/fr.po
@@ -259,9 +259,9 @@ msgstr ""
#. module: project_issue
#: view:project.issue:0
-#: field:project.issue,type_id:0
+#: field:project.issue,version_id:0
#: view:project.issue.report:0
-#: field:project.issue.report,type_id:0
+#: field:project.issue.report,version_id:0
msgid "Version"
msgstr ""
diff --git a/addons/project_issue/i18n/project_issue.pot b/addons/project_issue/i18n/project_issue.pot
index d409b33816c..521eefb19c8 100644
--- a/addons/project_issue/i18n/project_issue.pot
+++ b/addons/project_issue/i18n/project_issue.pot
@@ -254,9 +254,9 @@ msgstr ""
#. module: project_issue
#: view:project.issue:0
-#: field:project.issue,type_id:0
+#: field:project.issue,version_id:0
#: view:project.issue.report:0
-#: field:project.issue.report,type_id:0
+#: field:project.issue.report,version_id:0
msgid "Version"
msgstr ""
diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py
index 19db547983b..17df3444b27 100644
--- a/addons/project_issue/project_issue.py
+++ b/addons/project_issue/project_issue.py
@@ -28,6 +28,18 @@ import time
import tools
+class project_issue_version(osv.osv):
+ _name = "project.issue.version"
+ _order = "name desc"
+ _columns = {
+ 'name': fields.char('Version Number', size=32, required=True),
+ 'active': fields.boolean('Active', required=False),
+ }
+ _defaults = {
+ 'active': 1,
+ }
+project_issue_version()
+
class project_issue(crm.crm_case, osv.osv):
_name = "project.issue"
_description = "Project Issue"
@@ -161,7 +173,7 @@ class project_issue(crm.crm_case, osv.osv):
" With each commercial opportunity, you can indicate the canall which is this opportunity source."),
'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Severity'),
- 'type_id': fields.many2one('crm.case.resource.type', 'Version', domain="[('object_id.model', '=', 'project.issue')]"),
+ 'version_id': fields.many2one('project.issue.version', 'Version'),
'partner_name': fields.char("Employee's Name", size=64),
'partner_mobile': fields.char('Mobile', size=32),
'partner_phone': fields.char('Phone', size=32),
diff --git a/addons/project_issue/project_issue_data.xml b/addons/project_issue/project_issue_data.xml
index b5fa80b96bd..738f734e35a 100644
--- a/addons/project_issue/project_issue_data.xml
+++ b/addons/project_issue/project_issue_data.xml
@@ -19,19 +19,16 @@
-
- Version 4.2
-
+
+ v1.0
-
- Version 5.0
-
+
+ v2.0
-
- Version 6.0
-
+
+ v3.0
diff --git a/addons/project_issue/project_issue_demo.xml b/addons/project_issue/project_issue_demo.xml
index ad6af0e02b0..7c5ae6083ed 100644
--- a/addons/project_issue/project_issue_demo.xml
+++ b/addons/project_issue/project_issue_demo.xml
@@ -5,7 +5,6 @@
-
@@ -23,7 +22,6 @@
-
@@ -40,7 +38,6 @@
-
@@ -56,7 +53,6 @@
-
@@ -72,7 +68,6 @@
-
@@ -89,7 +84,6 @@
-
@@ -106,7 +100,6 @@
-
@@ -123,7 +116,6 @@
-
@@ -140,7 +132,6 @@
-
@@ -156,7 +147,6 @@
-
@@ -173,7 +163,6 @@
-
@@ -190,7 +179,6 @@
diff --git a/addons/sale/test/sale_report.yml b/addons/sale/test/sale_report.yml
index d60420294fe..8b55c21ec23 100644
--- a/addons/sale/test/sale_report.yml
+++ b/addons/sale/test/sale_report.yml
@@ -1,16 +1,8 @@
-
In order to test the PDF reports defined on a sale order, we will print a sale order
--
+-
!python {model: sale.order}: |
import netsvc, tools, os
(data, format) = netsvc.LocalService('report.sale.order').create(cr, uid, [ref('sale.order'),ref('sale.order2')], {}, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'sale-sale_order.'+format), 'wb+').write(data)
--
- In order to test the PDF reports defined on a sale order, we will print a report for sale prepare allot
--
- !python {model: sale.order}: |
- import netsvc, tools, os
- (data, format) = netsvc.LocalService('report.sale.order.prepare.allot').create(cr, uid, [ref('sale.order'),ref('sale.order2'),ref('sale.order3')], {}, {})
- if tools.config['test_report_directory']:
- file(os.path.join(tools.config['test_report_directory'], 'sale-sale_prepare_allot.'+format), 'wb+').write(data)
\ No newline at end of file
diff --git a/addons/test2.text b/addons/test2.text
new file mode 100644
index 00000000000..535d2b01d33
--- /dev/null
+++ b/addons/test2.text
@@ -0,0 +1,8 @@
+1
+2
+3
+4
+5
+6
+7
+8
diff --git a/addons/wiki/__openerp__.py b/addons/wiki/__openerp__.py
index 72f227ddcd7..f16347c4da9 100644
--- a/addons/wiki/__openerp__.py
+++ b/addons/wiki/__openerp__.py
@@ -49,5 +49,6 @@ keep track for the wiki groups, pages, and history
'installable': True,
'active': False,
'certificate': '0086363630317',
+ 'web': True,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/wiki/web/__init__.py b/addons/wiki/web/__init__.py
new file mode 100644
index 00000000000..5b55f6cf43b
--- /dev/null
+++ b/addons/wiki/web/__init__.py
@@ -0,0 +1,2 @@
+import widgets
+import controllers
diff --git a/addons/wiki/web/controllers/__init__.py b/addons/wiki/web/controllers/__init__.py
new file mode 100644
index 00000000000..8c163ddd260
--- /dev/null
+++ b/addons/wiki/web/controllers/__init__.py
@@ -0,0 +1 @@
+import wiki
diff --git a/addons/wiki/web/controllers/wiki.py b/addons/wiki/web/controllers/wiki.py
new file mode 100644
index 00000000000..a40952360ed
--- /dev/null
+++ b/addons/wiki/web/controllers/wiki.py
@@ -0,0 +1,65 @@
+###############################################################################
+#
+# Copyright (C) 2007-TODAY Tiny ERP Pvt Ltd. All Rights Reserved.
+#
+# $Id$
+#
+# Developed by Tiny (http://openerp.com) and Axelor (http://axelor.com).
+#
+# The OpenERP web client is distributed under the "OpenERP Public License".
+# It's based on Mozilla Public License Version (MPL) 1.1 with following
+# restrictions:
+#
+# - All names, links and logos of Tiny, OpenERP and Axelor must be
+# kept as in original distribution without any changes in all software
+# screens, especially in start-up page and the software header, even if
+# the application source code has been changed or updated or code has been
+# added.
+#
+# - All distributions of the software must keep source code with OEPL.
+#
+# - All integrations to any other software must keep source code with OEPL.
+#
+# If you need commercial licence to remove this kind of restriction please
+# contact us.
+#
+# You can see the MPL licence at: http://www.mozilla.org/MPL/MPL-1.1.html
+#
+###############################################################################
+
+import base64
+
+import cherrypy
+
+from openobject.tools import expose
+
+from openerp.utils import rpc
+
+from openerp.controllers import SecuredController
+
+
+class WikiView(SecuredController):
+ _cp_path = "/wiki/wiki"
+
+ def get_attachment(self, **kwargs):
+ attachments = rpc.RPCProxy('ir.attachment')
+ file_name = kwargs.get('file').replace("'", '').strip()
+ id = kwargs.get('id').strip()
+
+ ids = attachments.search([('datas_fname', '=', file_name),
+ ('res_model', '=', 'wiki.wiki'),
+ ('res_id', '=', id)])
+
+ res = attachments.read(ids, ['datas'])[0].get('datas')
+ return res, file_name
+
+ @expose(content_type='application/octet')
+ def getImage(self, *kw, **kws):
+ res, _ = self.get_attachment(**kws)
+ return base64.decodestring(res)
+
+ @expose(content_type='application/octet')
+ def getfile(self, *kw, **kws):
+ res, file_name = self.get_attachment(**kws)
+ cherrypy.response.headers['Content-Disposition'] = 'filename="%s"' % (file_name,)
+ return base64.decodestring(res)
diff --git a/addons/wiki/web/locales/de_DE/LC_MESSAGES/javascript.po b/addons/wiki/web/locales/de_DE/LC_MESSAGES/javascript.po
new file mode 100644
index 00000000000..ea8fd6999df
--- /dev/null
+++ b/addons/wiki/web/locales/de_DE/LC_MESSAGES/javascript.po
@@ -0,0 +1,20 @@
+# German (Germany) translations for PROJECT.
+# Copyright (C) 2009 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2009.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2009-11-18 17:21+0530\n"
+"PO-Revision-Date: 2010-08-02 17:53+0530\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: de_DE \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
diff --git a/addons/wiki/web/locales/de_DE/LC_MESSAGES/messages.po b/addons/wiki/web/locales/de_DE/LC_MESSAGES/messages.po
new file mode 100644
index 00000000000..79b06791b65
--- /dev/null
+++ b/addons/wiki/web/locales/de_DE/LC_MESSAGES/messages.po
@@ -0,0 +1,24 @@
+# German (Germany) translations for PROJECT.
+# Copyright (C) 2009 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2009.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2009-11-18 17:21+0530\n"
+"PO-Revision-Date: 2010-08-02 17:53+0530\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: de_DE \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
+#: widgets/wikimarkup/__init__.py:1981
+msgid "Table of Contents"
+msgstr ""
+
diff --git a/addons/wiki/web/locales/es_ES/LC_MESSAGES/javascript.po b/addons/wiki/web/locales/es_ES/LC_MESSAGES/javascript.po
new file mode 100644
index 00000000000..0c44218f06a
--- /dev/null
+++ b/addons/wiki/web/locales/es_ES/LC_MESSAGES/javascript.po
@@ -0,0 +1,20 @@
+# Spanish (Spain) translations for PROJECT.
+# Copyright (C) 2009 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2009.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2009-11-18 17:21+0530\n"
+"PO-Revision-Date: 2010-08-02 17:53+0530\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: es_ES \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
diff --git a/addons/wiki/web/locales/es_ES/LC_MESSAGES/messages.po b/addons/wiki/web/locales/es_ES/LC_MESSAGES/messages.po
new file mode 100644
index 00000000000..72f8c72bbf0
--- /dev/null
+++ b/addons/wiki/web/locales/es_ES/LC_MESSAGES/messages.po
@@ -0,0 +1,24 @@
+# Spanish (Spain) translations for PROJECT.
+# Copyright (C) 2009 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2009.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2009-11-18 17:21+0530\n"
+"PO-Revision-Date: 2010-08-02 17:53+0530\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: es_ES \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
+#: widgets/wikimarkup/__init__.py:1981
+msgid "Table of Contents"
+msgstr ""
+
diff --git a/addons/wiki/web/locales/fr_FR/LC_MESSAGES/javascript.po b/addons/wiki/web/locales/fr_FR/LC_MESSAGES/javascript.po
new file mode 100644
index 00000000000..31f9c5eda45
--- /dev/null
+++ b/addons/wiki/web/locales/fr_FR/LC_MESSAGES/javascript.po
@@ -0,0 +1,20 @@
+# French (France) translations for PROJECT.
+# Copyright (C) 2009 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2009.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2009-11-18 17:21+0530\n"
+"PO-Revision-Date: 2010-08-02 17:53+0530\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: fr_FR \n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
diff --git a/addons/wiki/web/locales/fr_FR/LC_MESSAGES/messages.po b/addons/wiki/web/locales/fr_FR/LC_MESSAGES/messages.po
new file mode 100644
index 00000000000..181738f734f
--- /dev/null
+++ b/addons/wiki/web/locales/fr_FR/LC_MESSAGES/messages.po
@@ -0,0 +1,24 @@
+# French (France) translations for PROJECT.
+# Copyright (C) 2009 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2009.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2009-11-18 17:21+0530\n"
+"PO-Revision-Date: 2010-08-02 17:53+0530\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: fr_FR \n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
+#: widgets/wikimarkup/__init__.py:1981
+msgid "Table of Contents"
+msgstr ""
+
diff --git a/addons/wiki/web/locales/javascript.pot b/addons/wiki/web/locales/javascript.pot
new file mode 100644
index 00000000000..725ce7a4883
--- /dev/null
+++ b/addons/wiki/web/locales/javascript.pot
@@ -0,0 +1,19 @@
+# Translations template for PROJECT.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2010.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-08-02 17:52+0530\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
diff --git a/addons/wiki/web/locales/messages.pot b/addons/wiki/web/locales/messages.pot
new file mode 100644
index 00000000000..16c76567441
--- /dev/null
+++ b/addons/wiki/web/locales/messages.pot
@@ -0,0 +1,23 @@
+# Translations template for PROJECT.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2010.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-08-02 17:52+0530\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel None\n"
+
+#: widgets/wikimarkup/__init__.py:1981
+msgid "Table of Contents"
+msgstr ""
+
diff --git a/addons/wiki/web/static/css/wiki.css b/addons/wiki/web/static/css/wiki.css
new file mode 100644
index 00000000000..51a649354d6
--- /dev/null
+++ b/addons/wiki/web/static/css/wiki.css
@@ -0,0 +1,32 @@
+.wikiwidget #toc {
+ border:1px solid #aaaaaa;
+ background-color:#f9f9f9;
+ padding:5px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: black;
+ background: none;
+ font-weight: normal;
+ margin: 0;
+ padding-top: .5em;
+ padding-bottom: .17em;
+ border-bottom: 1px solid #aaa;
+}
+h1 { font-size: 188%; }
+h2 { font-size: 150%; }
+h3, h4, h5, h6 {
+ border-bottom: none;
+}
+h3 { font-size: 132%; }
+h4 { font-size: 116%; }
+h5 { font-size: 100%; }
+h6 { font-size: 80%; }
+
+pre {
+ background-color:#F9F9F9;
+ border:1px dashed #2F6FAB;
+ color:black;
+ line-height:1.1em;
+ padding:1em;
+}
diff --git a/addons/wiki/web/widgets/__init__.py b/addons/wiki/web/widgets/__init__.py
new file mode 100644
index 00000000000..a3caa17e700
--- /dev/null
+++ b/addons/wiki/web/widgets/__init__.py
@@ -0,0 +1 @@
+from wiki import WikiWidget
diff --git a/addons/wiki/web/widgets/rss/__init__.py b/addons/wiki/web/widgets/rss/__init__.py
new file mode 100644
index 00000000000..464c4364414
--- /dev/null
+++ b/addons/wiki/web/widgets/rss/__init__.py
@@ -0,0 +1 @@
+import feedparser
\ No newline at end of file
diff --git a/addons/wiki/web/widgets/rss/feedparser.py b/addons/wiki/web/widgets/rss/feedparser.py
new file mode 100755
index 00000000000..6f30574d300
--- /dev/null
+++ b/addons/wiki/web/widgets/rss/feedparser.py
@@ -0,0 +1,2858 @@
+#!/usr/bin/env python
+"""Universal feed parser
+
+Handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds
+
+Visit http://feedparser.org/ for the latest version
+Visit http://feedparser.org/docs/ for the latest documentation
+
+Required: Python 2.1 or later
+Recommended: Python 2.3 or later
+Recommended: CJKCodecs and iconv_codec
+"""
+
+__version__ = "4.1"# + "$Revision: 1.92 $"[11:15] + "-cvs"
+__license__ = """Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE."""
+__author__ = "Mark Pilgrim "
+__contributors__ = ["Jason Diamond ",
+ "John Beimler ",
+ "Fazal Majid ",
+ "Aaron Swartz ",
+ "Kevin Marks "]
+_debug = 0
+
+# HTTP "User-Agent" header to send to servers when downloading feeds.
+# If you are embedding feedparser in a larger application, you should
+# change this to your application name and URL.
+USER_AGENT = "UniversalFeedParser/%s +http://feedparser.org/" % __version__
+
+# HTTP "Accept" header to send to servers when downloading feeds. If you don't
+# want to send an Accept header, set this to None.
+ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1"
+
+# List of preferred XML parsers, by SAX driver name. These will be tried first,
+# but if they're not installed, Python will keep searching through its own list
+# of pre-installed parsers until it finds one that supports everything we need.
+PREFERRED_XML_PARSERS = ["drv_libxml2"]
+
+# If you want feedparser to automatically run HTML markup through HTML Tidy, set
+# this to 1. Requires mxTidy
+# or utidylib .
+TIDY_MARKUP = 0
+
+# List of Python interfaces for HTML Tidy, in order of preference. Only useful
+# if TIDY_MARKUP = 1
+PREFERRED_TIDY_INTERFACES = ["uTidy", "mxTidy"]
+
+# ---------- required modules (should come with any Python distribution) ----------
+import sgmllib, re, sys, copy, urlparse, time, rfc822, types, cgi, urllib, urllib2
+try:
+ from cStringIO import StringIO as _StringIO
+except:
+ from StringIO import StringIO as _StringIO
+
+# ---------- optional modules (feedparser will work without these, but with reduced functionality) ----------
+
+# gzip is included with most Python distributions, but may not be available if you compiled your own
+try:
+ import gzip
+except:
+ gzip = None
+try:
+ import zlib
+except:
+ zlib = None
+
+# If a real XML parser is available, feedparser will attempt to use it. feedparser has
+# been tested with the built-in SAX parser, PyXML, and libxml2. On platforms where the
+# Python distribution does not come with an XML parser (such as Mac OS X 10.2 and some
+# versions of FreeBSD), feedparser will quietly fall back on regex-based parsing.
+try:
+ import xml.sax
+ xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers
+ from xml.sax.saxutils import escape as _xmlescape
+ _XML_AVAILABLE = 1
+except:
+ _XML_AVAILABLE = 0
+ def _xmlescape(data):
+ data = data.replace('&', '&')
+ data = data.replace('>', '>')
+ data = data.replace('<', '<')
+ return data
+
+# base64 support for Atom feeds that contain embedded binary data
+try:
+ import base64, binascii
+except:
+ base64 = binascii = None
+
+# cjkcodecs and iconv_codec provide support for more character encodings.
+# Both are available from http://cjkpython.i18n.org/
+try:
+ import cjkcodecs.aliases
+except:
+ pass
+try:
+ import iconv_codec
+except:
+ pass
+
+# chardet library auto-detects character encodings
+# Download from http://chardet.feedparser.org/
+try:
+ import chardet
+ if _debug:
+ import chardet.constants
+ chardet.constants._debug = 1
+except:
+ chardet = None
+
+# ---------- don't touch these ----------
+class ThingsNobodyCaresAboutButMe(Exception): pass
+class CharacterEncodingOverride(ThingsNobodyCaresAboutButMe): pass
+class CharacterEncodingUnknown(ThingsNobodyCaresAboutButMe): pass
+class NonXMLContentType(ThingsNobodyCaresAboutButMe): pass
+class UndeclaredNamespace(Exception): pass
+
+sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
+sgmllib.special = re.compile('' % (tag, ''.join([' %s="%s"' % t for t in attrs])), escape=0)
+
+ # match namespaces
+ if tag.find(':') <> -1:
+ prefix, suffix = tag.split(':', 1)
+ else:
+ prefix, suffix = '', tag
+ prefix = self.namespacemap.get(prefix, prefix)
+ if prefix:
+ prefix = prefix + '_'
+
+ # special hack for better tracking of empty textinput/image elements in illformed feeds
+ if (not prefix) and tag not in ('title', 'link', 'description', 'name'):
+ self.intextinput = 0
+ if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
+ self.inimage = 0
+
+ # call special handler (if defined) or default handler
+ methodname = '_start_' + prefix + suffix
+ try:
+ method = getattr(self, methodname)
+ return method(attrsD)
+ except AttributeError:
+ return self.push(prefix + suffix, 1)
+
+ def unknown_endtag(self, tag):
+ if _debug: sys.stderr.write('end %s\n' % tag)
+ # match namespaces
+ if tag.find(':') <> -1:
+ prefix, suffix = tag.split(':', 1)
+ else:
+ prefix, suffix = '', tag
+ prefix = self.namespacemap.get(prefix, prefix)
+ if prefix:
+ prefix = prefix + '_'
+
+ # call special handler (if defined) or default handler
+ methodname = '_end_' + prefix + suffix
+ try:
+ method = getattr(self, methodname)
+ method()
+ except AttributeError:
+ self.pop(prefix + suffix)
+
+ # track inline content
+ if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
+ # element declared itself as escaped markup, but it isn't really
+ self.contentparams['type'] = 'application/xhtml+xml'
+ if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
+ tag = tag.split(':')[-1]
+ self.handle_data('%s>' % tag, escape=0)
+
+ # track xml:base and xml:lang going out of scope
+ if self.basestack:
+ self.basestack.pop()
+ if self.basestack and self.basestack[-1]:
+ self.baseuri = self.basestack[-1]
+ if self.langstack:
+ self.langstack.pop()
+ if self.langstack: # and (self.langstack[-1] is not None):
+ self.lang = self.langstack[-1]
+
+ def handle_charref(self, ref):
+ # called for each character reference, e.g. for ' ', ref will be '160'
+ if not self.elementstack: return
+ ref = ref.lower()
+ if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'):
+ text = '%s;' % ref
+ else:
+ if ref[0] == 'x':
+ c = int(ref[1:], 16)
+ else:
+ c = int(ref)
+ text = unichr(c).encode('utf-8')
+ self.elementstack[-1][2].append(text)
+
+ def handle_entityref(self, ref):
+ # called for each entity reference, e.g. for '©', ref will be 'copy'
+ if not self.elementstack: return
+ if _debug: sys.stderr.write('entering handle_entityref with %s\n' % ref)
+ if ref in ('lt', 'gt', 'quot', 'amp', 'apos'):
+ text = '&%s;' % ref
+ else:
+ # entity resolution graciously donated by Aaron Swartz
+ def name2cp(k):
+ import htmlentitydefs
+ if hasattr(htmlentitydefs, 'name2codepoint'): # requires Python 2.3
+ return htmlentitydefs.name2codepoint[k]
+ k = htmlentitydefs.entitydefs[k]
+ if k.startswith('') and k.endswith(';'):
+ return int(k[2:-1]) # not in latin-1
+ return ord(k)
+ try: name2cp(ref)
+ except KeyError: text = '&%s;' % ref
+ else: text = unichr(name2cp(ref)).encode('utf-8')
+ self.elementstack[-1][2].append(text)
+
+ def handle_data(self, text, escape=1):
+ # called for each block of plain text, i.e. outside of any tag and
+ # not containing any character or entity references
+ if not self.elementstack: return
+ if escape and self.contentparams.get('type') == 'application/xhtml+xml':
+ text = _xmlescape(text)
+ self.elementstack[-1][2].append(text)
+
+ def handle_comment(self, text):
+ # called for each comment, e.g.
+ pass
+
+ def handle_pi(self, text):
+ # called for each processing instruction, e.g.
+ pass
+
+ def handle_decl(self, text):
+ pass
+
+ def parse_declaration(self, i):
+ # override internal declaration handler to handle CDATA blocks
+ if _debug: sys.stderr.write('entering parse_declaration\n')
+ if self.rawdata[i:i+9] == '', i)
+ if k == -1: k = len(self.rawdata)
+ self.handle_data(_xmlescape(self.rawdata[i+9:k]), 0)
+ return k+3
+ else:
+ k = self.rawdata.find('>', i)
+ return k+1
+
+ def mapContentType(self, contentType):
+ contentType = contentType.lower()
+ if contentType == 'text':
+ contentType = 'text/plain'
+ elif contentType == 'html':
+ contentType = 'text/html'
+ elif contentType == 'xhtml':
+ contentType = 'application/xhtml+xml'
+ return contentType
+
+ def trackNamespace(self, prefix, uri):
+ loweruri = uri.lower()
+ if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version:
+ self.version = 'rss090'
+ if loweruri == 'http://purl.org/rss/1.0/' and not self.version:
+ self.version = 'rss10'
+ if loweruri == 'http://www.w3.org/2005/atom' and not self.version:
+ self.version = 'atom10'
+ if loweruri.find('backend.userland.com/rss') <> -1:
+ # match any backend.userland.com namespace
+ uri = 'http://backend.userland.com/rss'
+ loweruri = uri
+ if self._matchnamespaces.has_key(loweruri):
+ self.namespacemap[prefix] = self._matchnamespaces[loweruri]
+ self.namespacesInUse[self._matchnamespaces[loweruri]] = uri
+ else:
+ self.namespacesInUse[prefix or ''] = uri
+
+ def resolveURI(self, uri):
+ return _urljoin(self.baseuri or '', uri)
+
+ def decodeEntities(self, element, data):
+ return data
+
+ def push(self, element, expectingText):
+ self.elementstack.append([element, expectingText, []])
+
+ def pop(self, element, stripWhitespace=1):
+ if not self.elementstack: return
+ if self.elementstack[-1][0] != element: return
+
+ element, expectingText, pieces = self.elementstack.pop()
+ output = ''.join(pieces)
+ if stripWhitespace:
+ output = output.strip()
+ if not expectingText: return output
+
+ # decode base64 content
+ if base64 and self.contentparams.get('base64', 0):
+ try:
+ output = base64.decodestring(output)
+ except binascii.Error:
+ pass
+ except binascii.Incomplete:
+ pass
+
+ # resolve relative URIs
+ if (element in self.can_be_relative_uri) and output:
+ output = self.resolveURI(output)
+
+ # decode entities within embedded markup
+ if not self.contentparams.get('base64', 0):
+ output = self.decodeEntities(element, output)
+
+ # remove temporary cruft from contentparams
+ try:
+ del self.contentparams['mode']
+ except KeyError:
+ pass
+ try:
+ del self.contentparams['base64']
+ except KeyError:
+ pass
+
+ # resolve relative URIs within embedded markup
+ if self.mapContentType(self.contentparams.get('type', 'text/html')) in self.html_types:
+ if element in self.can_contain_relative_uris:
+ output = _resolveRelativeURIs(output, self.baseuri, self.encoding)
+
+ # sanitize embedded markup
+ if self.mapContentType(self.contentparams.get('type', 'text/html')) in self.html_types:
+ if element in self.can_contain_dangerous_markup:
+ output = _sanitizeHTML(output, self.encoding)
+
+ if self.encoding and type(output) != type(u''):
+ try:
+ output = unicode(output, self.encoding)
+ except:
+ pass
+
+ # categories/tags/keywords/whatever are handled in _end_category
+ if element == 'category':
+ return output
+
+ # store output in appropriate place(s)
+ if self.inentry and not self.insource:
+ if element == 'content':
+ self.entries[-1].setdefault(element, [])
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ self.entries[-1][element].append(contentparams)
+ elif element == 'link':
+ self.entries[-1][element] = output
+ if output:
+ self.entries[-1]['links'][-1]['href'] = output
+ else:
+ if element == 'description':
+ element = 'summary'
+ self.entries[-1][element] = output
+ if self.incontent:
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ self.entries[-1][element + '_detail'] = contentparams
+ elif (self.infeed or self.insource) and (not self.intextinput) and (not self.inimage):
+ context = self._getContext()
+ if element == 'description':
+ element = 'subtitle'
+ context[element] = output
+ if element == 'link':
+ context['links'][-1]['href'] = output
+ elif self.incontent:
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ context[element + '_detail'] = contentparams
+ return output
+
+ def pushContent(self, tag, attrsD, defaultContentType, expectingText):
+ self.incontent += 1
+ self.contentparams = FeedParserDict({
+ 'type': self.mapContentType(attrsD.get('type', defaultContentType)),
+ 'language': self.lang,
+ 'base': self.baseuri})
+ self.contentparams['base64'] = self._isBase64(attrsD, self.contentparams)
+ self.push(tag, expectingText)
+
+ def popContent(self, tag):
+ value = self.pop(tag)
+ self.incontent -= 1
+ self.contentparams.clear()
+ return value
+
+ def _mapToStandardPrefix(self, name):
+ colonpos = name.find(':')
+ if colonpos <> -1:
+ prefix = name[:colonpos]
+ suffix = name[colonpos+1:]
+ prefix = self.namespacemap.get(prefix, prefix)
+ name = prefix + ':' + suffix
+ return name
+
+ def _getAttribute(self, attrsD, name):
+ return attrsD.get(self._mapToStandardPrefix(name))
+
+ def _isBase64(self, attrsD, contentparams):
+ if attrsD.get('mode', '') == 'base64':
+ return 1
+ if self.contentparams['type'].startswith('text/'):
+ return 0
+ if self.contentparams['type'].endswith('+xml'):
+ return 0
+ if self.contentparams['type'].endswith('/xml'):
+ return 0
+ return 1
+
+ def _itsAnHrefDamnIt(self, attrsD):
+ href = attrsD.get('url', attrsD.get('uri', attrsD.get('href', None)))
+ if href:
+ try:
+ del attrsD['url']
+ except KeyError:
+ pass
+ try:
+ del attrsD['uri']
+ except KeyError:
+ pass
+ attrsD['href'] = href
+ return attrsD
+
+ def _save(self, key, value):
+ context = self._getContext()
+ context.setdefault(key, value)
+
+ def _start_rss(self, attrsD):
+ versionmap = {'0.91': 'rss091u',
+ '0.92': 'rss092',
+ '0.93': 'rss093',
+ '0.94': 'rss094'}
+ if not self.version:
+ attr_version = attrsD.get('version', '')
+ version = versionmap.get(attr_version)
+ if version:
+ self.version = version
+ elif attr_version.startswith('2.'):
+ self.version = 'rss20'
+ else:
+ self.version = 'rss'
+
+ def _start_dlhottitles(self, attrsD):
+ self.version = 'hotrss'
+
+ def _start_channel(self, attrsD):
+ self.infeed = 1
+ self._cdf_common(attrsD)
+ _start_feedinfo = _start_channel
+
+ def _cdf_common(self, attrsD):
+ if attrsD.has_key('lastmod'):
+ self._start_modified({})
+ self.elementstack[-1][-1] = attrsD['lastmod']
+ self._end_modified()
+ if attrsD.has_key('href'):
+ self._start_link({})
+ self.elementstack[-1][-1] = attrsD['href']
+ self._end_link()
+
+ def _start_feed(self, attrsD):
+ self.infeed = 1
+ versionmap = {'0.1': 'atom01',
+ '0.2': 'atom02',
+ '0.3': 'atom03'}
+ if not self.version:
+ attr_version = attrsD.get('version')
+ version = versionmap.get(attr_version)
+ if version:
+ self.version = version
+ else:
+ self.version = 'atom'
+
+ def _end_channel(self):
+ self.infeed = 0
+ _end_feed = _end_channel
+
+ def _start_image(self, attrsD):
+ self.inimage = 1
+ self.push('image', 0)
+ context = self._getContext()
+ context.setdefault('image', FeedParserDict())
+
+ def _end_image(self):
+ self.pop('image')
+ self.inimage = 0
+
+ def _start_textinput(self, attrsD):
+ self.intextinput = 1
+ self.push('textinput', 0)
+ context = self._getContext()
+ context.setdefault('textinput', FeedParserDict())
+ _start_textInput = _start_textinput
+
+ def _end_textinput(self):
+ self.pop('textinput')
+ self.intextinput = 0
+ _end_textInput = _end_textinput
+
+ def _start_author(self, attrsD):
+ self.inauthor = 1
+ self.push('author', 1)
+ _start_managingeditor = _start_author
+ _start_dc_author = _start_author
+ _start_dc_creator = _start_author
+ _start_itunes_author = _start_author
+
+ def _end_author(self):
+ self.pop('author')
+ self.inauthor = 0
+ self._sync_author_detail()
+ _end_managingeditor = _end_author
+ _end_dc_author = _end_author
+ _end_dc_creator = _end_author
+ _end_itunes_author = _end_author
+
+ def _start_itunes_owner(self, attrsD):
+ self.inpublisher = 1
+ self.push('publisher', 0)
+
+ def _end_itunes_owner(self):
+ self.pop('publisher')
+ self.inpublisher = 0
+ self._sync_author_detail('publisher')
+
+ def _start_contributor(self, attrsD):
+ self.incontributor = 1
+ context = self._getContext()
+ context.setdefault('contributors', [])
+ context['contributors'].append(FeedParserDict())
+ self.push('contributor', 0)
+
+ def _end_contributor(self):
+ self.pop('contributor')
+ self.incontributor = 0
+
+ def _start_dc_contributor(self, attrsD):
+ self.incontributor = 1
+ context = self._getContext()
+ context.setdefault('contributors', [])
+ context['contributors'].append(FeedParserDict())
+ self.push('name', 0)
+
+ def _end_dc_contributor(self):
+ self._end_name()
+ self.incontributor = 0
+
+ def _start_name(self, attrsD):
+ self.push('name', 0)
+ _start_itunes_name = _start_name
+
+ def _end_name(self):
+ value = self.pop('name')
+ if self.inpublisher:
+ self._save_author('name', value, 'publisher')
+ elif self.inauthor:
+ self._save_author('name', value)
+ elif self.incontributor:
+ self._save_contributor('name', value)
+ elif self.intextinput:
+ context = self._getContext()
+ context['textinput']['name'] = value
+ _end_itunes_name = _end_name
+
+ def _start_width(self, attrsD):
+ self.push('width', 0)
+
+ def _end_width(self):
+ value = self.pop('width')
+ try:
+ value = int(value)
+ except:
+ value = 0
+ if self.inimage:
+ context = self._getContext()
+ context['image']['width'] = value
+
+ def _start_height(self, attrsD):
+ self.push('height', 0)
+
+ def _end_height(self):
+ value = self.pop('height')
+ try:
+ value = int(value)
+ except:
+ value = 0
+ if self.inimage:
+ context = self._getContext()
+ context['image']['height'] = value
+
+ def _start_url(self, attrsD):
+ self.push('href', 1)
+ _start_homepage = _start_url
+ _start_uri = _start_url
+
+ def _end_url(self):
+ value = self.pop('href')
+ if self.inauthor:
+ self._save_author('href', value)
+ elif self.incontributor:
+ self._save_contributor('href', value)
+ elif self.inimage:
+ context = self._getContext()
+ context['image']['href'] = value
+ elif self.intextinput:
+ context = self._getContext()
+ context['textinput']['link'] = value
+ _end_homepage = _end_url
+ _end_uri = _end_url
+
+ def _start_email(self, attrsD):
+ self.push('email', 0)
+ _start_itunes_email = _start_email
+
+ def _end_email(self):
+ value = self.pop('email')
+ if self.inpublisher:
+ self._save_author('email', value, 'publisher')
+ elif self.inauthor:
+ self._save_author('email', value)
+ elif self.incontributor:
+ self._save_contributor('email', value)
+ _end_itunes_email = _end_email
+
+ def _getContext(self):
+ if self.insource:
+ context = self.sourcedata
+ elif self.inentry:
+ context = self.entries[-1]
+ else:
+ context = self.feeddata
+ return context
+
+ def _save_author(self, key, value, prefix='author'):
+ context = self._getContext()
+ context.setdefault(prefix + '_detail', FeedParserDict())
+ context[prefix + '_detail'][key] = value
+ self._sync_author_detail()
+
+ def _save_contributor(self, key, value):
+ context = self._getContext()
+ context.setdefault('contributors', [FeedParserDict()])
+ context['contributors'][-1][key] = value
+
+ def _sync_author_detail(self, key='author'):
+ context = self._getContext()
+ detail = context.get('%s_detail' % key)
+ if detail:
+ name = detail.get('name')
+ email = detail.get('email')
+ if name and email:
+ context[key] = '%s (%s)' % (name, email)
+ elif name:
+ context[key] = name
+ elif email:
+ context[key] = email
+ else:
+ author = context.get(key)
+ if not author: return
+ emailmatch = re.search(r'''(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))''', author)
+ if not emailmatch: return
+ email = emailmatch.group(0)
+ # probably a better way to do the following, but it passes all the tests
+ author = author.replace(email, '')
+ author = author.replace('()', '')
+ author = author.strip()
+ if author and (author[0] == '('):
+ author = author[1:]
+ if author and (author[-1] == ')'):
+ author = author[:-1]
+ author = author.strip()
+ context.setdefault('%s_detail' % key, FeedParserDict())
+ context['%s_detail' % key]['name'] = author
+ context['%s_detail' % key]['email'] = email
+
+ def _start_subtitle(self, attrsD):
+ self.pushContent('subtitle', attrsD, 'text/plain', 1)
+ _start_tagline = _start_subtitle
+ _start_itunes_subtitle = _start_subtitle
+
+ def _end_subtitle(self):
+ self.popContent('subtitle')
+ _end_tagline = _end_subtitle
+ _end_itunes_subtitle = _end_subtitle
+
+ def _start_rights(self, attrsD):
+ self.pushContent('rights', attrsD, 'text/plain', 1)
+ _start_dc_rights = _start_rights
+ _start_copyright = _start_rights
+
+ def _end_rights(self):
+ self.popContent('rights')
+ _end_dc_rights = _end_rights
+ _end_copyright = _end_rights
+
+ def _start_item(self, attrsD):
+ self.entries.append(FeedParserDict())
+ self.push('item', 0)
+ self.inentry = 1
+ self.guidislink = 0
+ id = self._getAttribute(attrsD, 'rdf:about')
+ if id:
+ context = self._getContext()
+ context['id'] = id
+ self._cdf_common(attrsD)
+ _start_entry = _start_item
+ _start_product = _start_item
+
+ def _end_item(self):
+ self.pop('item')
+ self.inentry = 0
+ _end_entry = _end_item
+
+ def _start_dc_language(self, attrsD):
+ self.push('language', 1)
+ _start_language = _start_dc_language
+
+ def _end_dc_language(self):
+ self.lang = self.pop('language')
+ _end_language = _end_dc_language
+
+ def _start_dc_publisher(self, attrsD):
+ self.push('publisher', 1)
+ _start_webmaster = _start_dc_publisher
+
+ def _end_dc_publisher(self):
+ self.pop('publisher')
+ self._sync_author_detail('publisher')
+ _end_webmaster = _end_dc_publisher
+
+ def _start_published(self, attrsD):
+ self.push('published', 1)
+ _start_dcterms_issued = _start_published
+ _start_issued = _start_published
+
+ def _end_published(self):
+ value = self.pop('published')
+ self._save('published_parsed', _parse_date(value))
+ _end_dcterms_issued = _end_published
+ _end_issued = _end_published
+
+ def _start_updated(self, attrsD):
+ self.push('updated', 1)
+ _start_modified = _start_updated
+ _start_dcterms_modified = _start_updated
+ _start_pubdate = _start_updated
+ _start_dc_date = _start_updated
+
+ def _end_updated(self):
+ value = self.pop('updated')
+ parsed_value = _parse_date(value)
+ self._save('updated_parsed', parsed_value)
+ _end_modified = _end_updated
+ _end_dcterms_modified = _end_updated
+ _end_pubdate = _end_updated
+ _end_dc_date = _end_updated
+
+ def _start_created(self, attrsD):
+ self.push('created', 1)
+ _start_dcterms_created = _start_created
+
+ def _end_created(self):
+ value = self.pop('created')
+ self._save('created_parsed', _parse_date(value))
+ _end_dcterms_created = _end_created
+
+ def _start_expirationdate(self, attrsD):
+ self.push('expired', 1)
+
+ def _end_expirationdate(self):
+ self._save('expired_parsed', _parse_date(self.pop('expired')))
+
+ def _start_cc_license(self, attrsD):
+ self.push('license', 1)
+ value = self._getAttribute(attrsD, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('license')
+
+ def _start_creativecommons_license(self, attrsD):
+ self.push('license', 1)
+
+ def _end_creativecommons_license(self):
+ self.pop('license')
+
+ def _addTag(self, term, scheme, label):
+ context = self._getContext()
+ tags = context.setdefault('tags', [])
+ if (not term) and (not scheme) and (not label): return
+ value = FeedParserDict({'term': term, 'scheme': scheme, 'label': label})
+ if value not in tags:
+ tags.append(FeedParserDict({'term': term, 'scheme': scheme, 'label': label}))
+
+ def _start_category(self, attrsD):
+ if _debug: sys.stderr.write('entering _start_category with %s\n' % repr(attrsD))
+ term = attrsD.get('term')
+ scheme = attrsD.get('scheme', attrsD.get('domain'))
+ label = attrsD.get('label')
+ self._addTag(term, scheme, label)
+ self.push('category', 1)
+ _start_dc_subject = _start_category
+ _start_keywords = _start_category
+
+ def _end_itunes_keywords(self):
+ for term in self.pop('itunes_keywords').split():
+ self._addTag(term, 'http://www.itunes.com/', None)
+
+ def _start_itunes_category(self, attrsD):
+ self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None)
+ self.push('category', 1)
+
+ def _end_category(self):
+ value = self.pop('category')
+ if not value: return
+ context = self._getContext()
+ tags = context['tags']
+ if value and len(tags) and not tags[-1]['term']:
+ tags[-1]['term'] = value
+ else:
+ self._addTag(value, None, None)
+ _end_dc_subject = _end_category
+ _end_keywords = _end_category
+ _end_itunes_category = _end_category
+
+ def _start_cloud(self, attrsD):
+ self._getContext()['cloud'] = FeedParserDict(attrsD)
+
+ def _start_link(self, attrsD):
+ attrsD.setdefault('rel', 'alternate')
+ attrsD.setdefault('type', 'text/html')
+ attrsD = self._itsAnHrefDamnIt(attrsD)
+ if attrsD.has_key('href'):
+ attrsD['href'] = self.resolveURI(attrsD['href'])
+ expectingText = self.infeed or self.inentry or self.insource
+ context = self._getContext()
+ context.setdefault('links', [])
+ context['links'].append(FeedParserDict(attrsD))
+ if attrsD['rel'] == 'enclosure':
+ self._start_enclosure(attrsD)
+ if attrsD.has_key('href'):
+ expectingText = 0
+ if (attrsD.get('rel') == 'alternate') and (self.mapContentType(attrsD.get('type')) in self.html_types):
+ context['link'] = attrsD['href']
+ else:
+ self.push('link', expectingText)
+ _start_producturl = _start_link
+
+ def _end_link(self):
+ value = self.pop('link')
+ context = self._getContext()
+ if self.intextinput:
+ context['textinput']['link'] = value
+ if self.inimage:
+ context['image']['link'] = value
+ _end_producturl = _end_link
+
+ def _start_guid(self, attrsD):
+ self.guidislink = (attrsD.get('ispermalink', 'true') == 'true')
+ self.push('id', 1)
+
+ def _end_guid(self):
+ value = self.pop('id')
+ self._save('guidislink', self.guidislink and not self._getContext().has_key('link'))
+ if self.guidislink:
+ # guid acts as link, but only if 'ispermalink' is not present or is 'true',
+ # and only if the item doesn't already have a link element
+ self._save('link', value)
+
+ def _start_title(self, attrsD):
+ self.pushContent('title', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
+ _start_dc_title = _start_title
+ _start_media_title = _start_title
+
+ def _end_title(self):
+ value = self.popContent('title')
+ context = self._getContext()
+ if self.intextinput:
+ context['textinput']['title'] = value
+ elif self.inimage:
+ context['image']['title'] = value
+ _end_dc_title = _end_title
+ _end_media_title = _end_title
+
+ def _start_description(self, attrsD):
+ context = self._getContext()
+ if context.has_key('summary'):
+ self._summaryKey = 'content'
+ self._start_content(attrsD)
+ else:
+ self.pushContent('description', attrsD, 'text/html', self.infeed or self.inentry or self.insource)
+
+ def _start_abstract(self, attrsD):
+ self.pushContent('description', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
+
+ def _end_description(self):
+ if self._summaryKey == 'content':
+ self._end_content()
+ else:
+ value = self.popContent('description')
+ context = self._getContext()
+ if self.intextinput:
+ context['textinput']['description'] = value
+ elif self.inimage:
+ context['image']['description'] = value
+ self._summaryKey = None
+ _end_abstract = _end_description
+
+ def _start_info(self, attrsD):
+ self.pushContent('info', attrsD, 'text/plain', 1)
+ _start_feedburner_browserfriendly = _start_info
+
+ def _end_info(self):
+ self.popContent('info')
+ _end_feedburner_browserfriendly = _end_info
+
+ def _start_generator(self, attrsD):
+ if attrsD:
+ attrsD = self._itsAnHrefDamnIt(attrsD)
+ if attrsD.has_key('href'):
+ attrsD['href'] = self.resolveURI(attrsD['href'])
+ self._getContext()['generator_detail'] = FeedParserDict(attrsD)
+ self.push('generator', 1)
+
+ def _end_generator(self):
+ value = self.pop('generator')
+ context = self._getContext()
+ if context.has_key('generator_detail'):
+ context['generator_detail']['name'] = value
+
+ def _start_admin_generatoragent(self, attrsD):
+ self.push('generator', 1)
+ value = self._getAttribute(attrsD, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('generator')
+ self._getContext()['generator_detail'] = FeedParserDict({'href': value})
+
+ def _start_admin_errorreportsto(self, attrsD):
+ self.push('errorreportsto', 1)
+ value = self._getAttribute(attrsD, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('errorreportsto')
+
+ def _start_summary(self, attrsD):
+ context = self._getContext()
+ if context.has_key('summary'):
+ self._summaryKey = 'content'
+ self._start_content(attrsD)
+ else:
+ self._summaryKey = 'summary'
+ self.pushContent(self._summaryKey, attrsD, 'text/plain', 1)
+ _start_itunes_summary = _start_summary
+
+ def _end_summary(self):
+ if self._summaryKey == 'content':
+ self._end_content()
+ else:
+ self.popContent(self._summaryKey or 'summary')
+ self._summaryKey = None
+ _end_itunes_summary = _end_summary
+
+ def _start_enclosure(self, attrsD):
+ attrsD = self._itsAnHrefDamnIt(attrsD)
+ self._getContext().setdefault('enclosures', []).append(FeedParserDict(attrsD))
+ href = attrsD.get('href')
+ if href:
+ context = self._getContext()
+ if not context.get('id'):
+ context['id'] = href
+
+ def _start_source(self, attrsD):
+ self.insource = 1
+
+ def _end_source(self):
+ self.insource = 0
+ self._getContext()['source'] = copy.deepcopy(self.sourcedata)
+ self.sourcedata.clear()
+
+ def _start_content(self, attrsD):
+ self.pushContent('content', attrsD, 'text/plain', 1)
+ src = attrsD.get('src')
+ if src:
+ self.contentparams['src'] = src
+ self.push('content', 1)
+
+ def _start_prodlink(self, attrsD):
+ self.pushContent('content', attrsD, 'text/html', 1)
+
+ def _start_body(self, attrsD):
+ self.pushContent('content', attrsD, 'application/xhtml+xml', 1)
+ _start_xhtml_body = _start_body
+
+ def _start_content_encoded(self, attrsD):
+ self.pushContent('content', attrsD, 'text/html', 1)
+ _start_fullitem = _start_content_encoded
+
+ def _end_content(self):
+ copyToDescription = self.mapContentType(self.contentparams.get('type')) in (['text/plain'] + self.html_types)
+ value = self.popContent('content')
+ if copyToDescription:
+ self._save('description', value)
+ _end_body = _end_content
+ _end_xhtml_body = _end_content
+ _end_content_encoded = _end_content
+ _end_fullitem = _end_content
+ _end_prodlink = _end_content
+
+ def _start_itunes_image(self, attrsD):
+ self.push('itunes_image', 0)
+ self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')})
+ _start_itunes_link = _start_itunes_image
+
+ def _end_itunes_block(self):
+ value = self.pop('itunes_block', 0)
+ self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0
+
+ def _end_itunes_explicit(self):
+ value = self.pop('itunes_explicit', 0)
+ self._getContext()['itunes_explicit'] = (value == 'yes') and 1 or 0
+
+if _XML_AVAILABLE:
+ class _StrictFeedParser(_FeedParserMixin, xml.sax.handler.ContentHandler):
+ def __init__(self, baseuri, baselang, encoding):
+ if _debug: sys.stderr.write('trying StrictFeedParser\n')
+ xml.sax.handler.ContentHandler.__init__(self)
+ _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
+ self.bozo = 0
+ self.exc = None
+
+ def startPrefixMapping(self, prefix, uri):
+ self.trackNamespace(prefix, uri)
+
+ def startElementNS(self, name, qname, attrs):
+ namespace, localname = name
+ lowernamespace = str(namespace or '').lower()
+ if lowernamespace.find('backend.userland.com/rss') <> -1:
+ # match any backend.userland.com namespace
+ namespace = 'http://backend.userland.com/rss'
+ lowernamespace = namespace
+ if qname and qname.find(':') > 0:
+ givenprefix = qname.split(':')[0]
+ else:
+ givenprefix = None
+ prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
+ if givenprefix and (prefix == None or (prefix == '' and lowernamespace == '')) and not self.namespacesInUse.has_key(givenprefix):
+ raise UndeclaredNamespace, "'%s' is not associated with a namespace" % givenprefix
+ if prefix:
+ localname = prefix + ':' + localname
+ localname = str(localname).lower()
+ if _debug: sys.stderr.write('startElementNS: qname = %s, namespace = %s, givenprefix = %s, prefix = %s, attrs = %s, localname = %s\n' % (qname, namespace, givenprefix, prefix, attrs.items(), localname))
+
+ # qname implementation is horribly broken in Python 2.1 (it
+ # doesn't report any), and slightly broken in Python 2.2 (it
+ # doesn't report the xml: namespace). So we match up namespaces
+ # with a known list first, and then possibly override them with
+ # the qnames the SAX parser gives us (if indeed it gives us any
+ # at all). Thanks to MatejC for helping me test this and
+ # tirelessly telling me that it didn't work yet.
+ attrsD = {}
+ for (namespace, attrlocalname), attrvalue in attrs._attrs.items():
+ lowernamespace = (namespace or '').lower()
+ prefix = self._matchnamespaces.get(lowernamespace, '')
+ if prefix:
+ attrlocalname = prefix + ':' + attrlocalname
+ attrsD[str(attrlocalname).lower()] = attrvalue
+ for qname in attrs.getQNames():
+ attrsD[str(qname).lower()] = attrs.getValueByQName(qname)
+ self.unknown_starttag(localname, attrsD.items())
+
+ def characters(self, text):
+ self.handle_data(text)
+
+ def endElementNS(self, name, qname):
+ namespace, localname = name
+ lowernamespace = str(namespace or '').lower()
+ if qname and qname.find(':') > 0:
+ givenprefix = qname.split(':')[0]
+ else:
+ givenprefix = ''
+ prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
+ if prefix:
+ localname = prefix + ':' + localname
+ localname = str(localname).lower()
+ self.unknown_endtag(localname)
+
+ def error(self, exc):
+ self.bozo = 1
+ self.exc = exc
+
+ def fatalError(self, exc):
+ self.error(exc)
+ raise exc
+
+class _BaseHTMLProcessor(sgmllib.SGMLParser):
+ elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr',
+ 'img', 'input', 'isindex', 'link', 'meta', 'param']
+
+ def __init__(self, encoding):
+ self.encoding = encoding
+ if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding)
+ sgmllib.SGMLParser.__init__(self)
+
+ def reset(self):
+ self.pieces = []
+ sgmllib.SGMLParser.reset(self)
+
+ def _shorttag_replace(self, match):
+ tag = match.group(1)
+ if tag in self.elements_no_end_tag:
+ return '<' + tag + ' />'
+ else:
+ return '<' + tag + '>' + tag + '>'
+
+ def feed(self, data):
+ data = re.compile(r'', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace
+ data = re.sub(r'<([^<\s]+?)\s*/>', self._shorttag_replace, data)
+ data = data.replace(''', "'")
+ data = data.replace('"', '"')
+ if self.encoding and type(data) == type(u''):
+ data = data.encode(self.encoding)
+ sgmllib.SGMLParser.feed(self, data)
+
+ def normalize_attrs(self, attrs):
+ # utility method to be called by descendants
+ attrs = [(k.lower(), v) for k, v in attrs]
+ attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
+ return attrs
+
+ def unknown_starttag(self, tag, attrs):
+ # called for each start tag
+ # attrs is a list of (attr, value) tuples
+ # e.g. for , tag='pre', attrs=[('class', 'screen')]
+ if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag)
+ uattrs = []
+ # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds
+ for key, value in attrs:
+ if type(value) != type(u''):
+ value = unicode(value, self.encoding)
+ uattrs.append((unicode(key, self.encoding), value))
+ strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs]).encode(self.encoding)
+ if tag in self.elements_no_end_tag:
+ self.pieces.append('<%(tag)s%(strattrs)s />' % locals())
+ else:
+ self.pieces.append('<%(tag)s%(strattrs)s>' % locals())
+
+ def unknown_endtag(self, tag):
+ # called for each end tag, e.g. for , tag will be 'pre'
+ # Reconstruct the original end tag.
+ if tag not in self.elements_no_end_tag:
+ self.pieces.append("%(tag)s>" % locals())
+
+ def handle_charref(self, ref):
+ # called for each character reference, e.g. for ' ', ref will be '160'
+ # Reconstruct the original character reference.
+ self.pieces.append('%(ref)s;' % locals())
+
+ def handle_entityref(self, ref):
+ # called for each entity reference, e.g. for '©', ref will be 'copy'
+ # Reconstruct the original entity reference.
+ self.pieces.append('&%(ref)s;' % locals())
+
+ def handle_data(self, text):
+ # called for each block of plain text, i.e. outside of any tag and
+ # not containing any character or entity references
+ # Store the original text verbatim.
+ if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' % text)
+ self.pieces.append(text)
+
+ def handle_comment(self, text):
+ # called for each HTML comment, e.g.
+ # Reconstruct the original comment.
+ self.pieces.append('' % locals())
+
+ def handle_pi(self, text):
+ # called for each processing instruction, e.g.
+ # Reconstruct original processing instruction.
+ self.pieces.append('%(text)s>' % locals())
+
+ def handle_decl(self, text):
+ # called for the DOCTYPE, if present, e.g.
+ #
+ # Reconstruct original DOCTYPE
+ self.pieces.append('' % locals())
+
+ _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
+ def _scan_name(self, i, declstartpos):
+ rawdata = self.rawdata
+ n = len(rawdata)
+ if i == n:
+ return None, -1
+ m = self._new_declname_match(rawdata, i)
+ if m:
+ s = m.group()
+ name = s.strip()
+ if (i + len(s)) == n:
+ return None, -1 # end of buffer
+ return name.lower(), m.end()
+ else:
+ self.handle_data(rawdata)
+# self.updatepos(declstartpos, i)
+ return None, -1
+
+ def output(self):
+ '''Return processed HTML as a single string'''
+ return ''.join([str(p) for p in self.pieces])
+
+class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
+ def __init__(self, baseuri, baselang, encoding):
+ sgmllib.SGMLParser.__init__(self)
+ _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
+
+ def decodeEntities(self, element, data):
+ data = data.replace('<', '<')
+ data = data.replace('<', '<')
+ data = data.replace('>', '>')
+ data = data.replace('>', '>')
+ data = data.replace('&', '&')
+ data = data.replace('&', '&')
+ data = data.replace('"', '"')
+ data = data.replace('"', '"')
+ data = data.replace(''', ''')
+ data = data.replace(''', ''')
+ if self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
+ data = data.replace('<', '<')
+ data = data.replace('>', '>')
+ data = data.replace('&', '&')
+ data = data.replace('"', '"')
+ data = data.replace(''', "'")
+ return data
+
+class _RelativeURIResolver(_BaseHTMLProcessor):
+ relative_uris = [('a', 'href'),
+ ('applet', 'codebase'),
+ ('area', 'href'),
+ ('blockquote', 'cite'),
+ ('body', 'background'),
+ ('del', 'cite'),
+ ('form', 'action'),
+ ('frame', 'longdesc'),
+ ('frame', 'src'),
+ ('iframe', 'longdesc'),
+ ('iframe', 'src'),
+ ('head', 'profile'),
+ ('img', 'longdesc'),
+ ('img', 'src'),
+ ('img', 'usemap'),
+ ('input', 'src'),
+ ('input', 'usemap'),
+ ('ins', 'cite'),
+ ('link', 'href'),
+ ('object', 'classid'),
+ ('object', 'codebase'),
+ ('object', 'data'),
+ ('object', 'usemap'),
+ ('q', 'cite'),
+ ('script', 'src')]
+
+ def __init__(self, baseuri, encoding):
+ _BaseHTMLProcessor.__init__(self, encoding)
+ self.baseuri = baseuri
+
+ def resolveURI(self, uri):
+ return _urljoin(self.baseuri, uri)
+
+ def unknown_starttag(self, tag, attrs):
+ attrs = self.normalize_attrs(attrs)
+ attrs = [(key, ((tag, key) in self.relative_uris) and self.resolveURI(value) or value) for key, value in attrs]
+ _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
+
+def _resolveRelativeURIs(htmlSource, baseURI, encoding):
+ if _debug: sys.stderr.write('entering _resolveRelativeURIs\n')
+ p = _RelativeURIResolver(baseURI, encoding)
+ p.feed(htmlSource)
+ return p.output()
+
+class _HTMLSanitizer(_BaseHTMLProcessor):
+ acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
+ 'blockquote', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col',
+ 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset',
+ 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input',
+ 'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup',
+ 'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small', 'span', 'strike',
+ 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th',
+ 'thead', 'tr', 'tt', 'u', 'ul', 'var']
+
+ acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
+ 'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 'cellspacing',
+ 'char', 'charoff', 'charset', 'checked', 'cite', 'class', 'clear', 'cols',
+ 'colspan', 'color', 'compact', 'coords', 'datetime', 'dir', 'disabled',
+ 'enctype', 'for', 'frame', 'headers', 'height', 'href', 'hreflang', 'hspace',
+ 'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'media', 'method',
+ 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly',
+ 'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
+ 'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 'type',
+ 'usemap', 'valign', 'value', 'vspace', 'width']
+
+ unacceptable_elements_with_end_tag = ['script', 'applet']
+
+ def reset(self):
+ _BaseHTMLProcessor.reset(self)
+ self.unacceptablestack = 0
+
+ def unknown_starttag(self, tag, attrs):
+ if not tag in self.acceptable_elements:
+ if tag in self.unacceptable_elements_with_end_tag:
+ self.unacceptablestack += 1
+ return
+ attrs = self.normalize_attrs(attrs)
+ attrs = [(key, value) for key, value in attrs if key in self.acceptable_attributes]
+ _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
+
+ def unknown_endtag(self, tag):
+ if not tag in self.acceptable_elements:
+ if tag in self.unacceptable_elements_with_end_tag:
+ self.unacceptablestack -= 1
+ return
+ _BaseHTMLProcessor.unknown_endtag(self, tag)
+
+ def handle_pi(self, text):
+ pass
+
+ def handle_decl(self, text):
+ pass
+
+ def handle_data(self, text):
+ if not self.unacceptablestack:
+ _BaseHTMLProcessor.handle_data(self, text)
+
+def _sanitizeHTML(htmlSource, encoding):
+ p = _HTMLSanitizer(encoding)
+ p.feed(htmlSource)
+ data = p.output()
+ if TIDY_MARKUP:
+ # loop through list of preferred Tidy interfaces looking for one that's installed,
+ # then set up a common _tidy function to wrap the interface-specific API.
+ _tidy = None
+ for tidy_interface in PREFERRED_TIDY_INTERFACES:
+ try:
+ if tidy_interface == "uTidy":
+ from tidy import parseString as _utidy
+ def _tidy(data, **kwargs):
+ return str(_utidy(data, **kwargs))
+ break
+ elif tidy_interface == "mxTidy":
+ from mx.Tidy import Tidy as _mxtidy
+ def _tidy(data, **kwargs):
+ nerrors, nwarnings, data, errordata = _mxtidy.tidy(data, **kwargs)
+ return data
+ break
+ except:
+ pass
+ if _tidy:
+ utf8 = type(data) == type(u'')
+ if utf8:
+ data = data.encode('utf-8')
+ data = _tidy(data, output_xhtml=1, numeric_entities=1, wrap=0, char_encoding="utf8")
+ if utf8:
+ data = unicode(data, 'utf-8')
+ if data.count(''):
+ data = data.split('>', 1)[1]
+ if data.count('= '2.3.3'
+ assert base64 != None
+ user, passw = base64.decodestring(req.headers['Authorization'].split(' ')[1]).split(':')
+ realm = re.findall('realm="([^"]*)"', headers['WWW-Authenticate'])[0]
+ self.add_password(realm, host, user, passw)
+ retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
+ self.reset_retry_count()
+ return retry
+ except:
+ return self.http_error_default(req, fp, code, msg, headers)
+
+def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers):
+ """URL, filename, or string --> stream
+
+ This function lets you define parsers that take any input source
+ (URL, pathname to local or network file, or actual data as a string)
+ and deal with it in a uniform manner. Returned object is guaranteed
+ to have all the basic stdio read methods (read, readline, readlines).
+ Just .close() the object when you're done with it.
+
+ If the etag argument is supplied, it will be used as the value of an
+ If-None-Match request header.
+
+ If the modified argument is supplied, it must be a tuple of 9 integers
+ as returned by gmtime() in the standard Python time module. This MUST
+ be in GMT (Greenwich Mean Time). The formatted date/time will be used
+ as the value of an If-Modified-Since request header.
+
+ If the agent argument is supplied, it will be used as the value of a
+ User-Agent request header.
+
+ If the referrer argument is supplied, it will be used as the value of a
+ Referer[sic] request header.
+
+ If handlers is supplied, it is a list of handlers used to build a
+ urllib2 opener.
+ """
+
+ if hasattr(url_file_stream_or_string, 'read'):
+ return url_file_stream_or_string
+
+ if url_file_stream_or_string == '-':
+ return sys.stdin
+
+ if urlparse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp'):
+ if not agent:
+ agent = USER_AGENT
+ # test for inline user:password for basic auth
+ auth = None
+ if base64:
+ urltype, rest = urllib.splittype(url_file_stream_or_string)
+ realhost, rest = urllib.splithost(rest)
+ if realhost:
+ user_passwd, realhost = urllib.splituser(realhost)
+ if user_passwd:
+ url_file_stream_or_string = '%s://%s%s' % (urltype, realhost, rest)
+ auth = base64.encodestring(user_passwd).strip()
+ # try to open with urllib2 (to use optional headers)
+ request = urllib2.Request(url_file_stream_or_string)
+ request.add_header('User-Agent', agent)
+ if etag:
+ request.add_header('If-None-Match', etag)
+ if modified:
+ # format into an RFC 1123-compliant timestamp. We can't use
+ # time.strftime() since the %a and %b directives can be affected
+ # by the current locale, but RFC 2616 states that dates must be
+ # in English.
+ short_weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+ months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ request.add_header('If-Modified-Since', '%s, %02d %s %04d %02d:%02d:%02d GMT' % (short_weekdays[modified[6]], modified[2], months[modified[1] - 1], modified[0], modified[3], modified[4], modified[5]))
+ if referrer:
+ request.add_header('Referer', referrer)
+ if gzip and zlib:
+ request.add_header('Accept-encoding', 'gzip, deflate')
+ elif gzip:
+ request.add_header('Accept-encoding', 'gzip')
+ elif zlib:
+ request.add_header('Accept-encoding', 'deflate')
+ else:
+ request.add_header('Accept-encoding', '')
+ if auth:
+ request.add_header('Authorization', 'Basic %s' % auth)
+ if ACCEPT_HEADER:
+ request.add_header('Accept', ACCEPT_HEADER)
+ request.add_header('A-IM', 'feed') # RFC 3229 support
+ opener = apply(urllib2.build_opener, tuple([_FeedURLHandler()] + handlers))
+ opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent
+ try:
+ return opener.open(request)
+ finally:
+ opener.close() # JohnD
+
+ # try to open with native open function (if url_file_stream_or_string is a filename)
+ try:
+ return open(url_file_stream_or_string)
+ except:
+ pass
+
+ # treat url_file_stream_or_string as string
+ return _StringIO(str(url_file_stream_or_string))
+
+_date_handlers = []
+def registerDateHandler(func):
+ '''Register a date handler function (takes string, returns 9-tuple date in GMT)'''
+ _date_handlers.insert(0, func)
+
+# ISO-8601 date parsing routines written by Fazal Majid.
+# The ISO 8601 standard is very convoluted and irregular - a full ISO 8601
+# parser is beyond the scope of feedparser and would be a worthwhile addition
+# to the Python library.
+# A single regular expression cannot parse ISO 8601 date formats into groups
+# as the standard is highly irregular (for instance is 030104 2003-01-04 or
+# 0301-04-01), so we use templates instead.
+# Please note the order in templates is significant because we need a
+# greedy match.
+_iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-MM', 'YYYY-?OOO',
+ 'YY-?MM-?DD', 'YY-?OOO', 'YYYY',
+ '-YY-?MM', '-OOO', '-YY',
+ '--MM-?DD', '--MM',
+ '---DD',
+ 'CC', '']
+_iso8601_re = [
+ tmpl.replace(
+ 'YYYY', r'(?P\d{4})').replace(
+ 'YY', r'(?P\d\d)').replace(
+ 'MM', r'(?P[01]\d)').replace(
+ 'DD', r'(?P[0123]\d)').replace(
+ 'OOO', r'(?P[0123]\d\d)').replace(
+ 'CC', r'(?P\d\d$)')
+ + r'(T?(?P\d{2}):(?P\d{2})'
+ + r'(:(?P\d{2}))?'
+ + r'(?P[+-](?P\d{2})(:(?P\d{2}))?|Z)?)?'
+ for tmpl in _iso8601_tmpl]
+del tmpl
+_iso8601_matches = [re.compile(regex).match for regex in _iso8601_re]
+del regex
+def _parse_date_iso8601(dateString):
+ '''Parse a variety of ISO-8601-compatible formats like 20040105'''
+ m = None
+ for _iso8601_match in _iso8601_matches:
+ m = _iso8601_match(dateString)
+ if m: break
+ if not m: return
+ if m.span() == (0, 0): return
+ params = m.groupdict()
+ ordinal = params.get('ordinal', 0)
+ if ordinal:
+ ordinal = int(ordinal)
+ else:
+ ordinal = 0
+ year = params.get('year', '--')
+ if not year or year == '--':
+ year = time.gmtime()[0]
+ elif len(year) == 2:
+ # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993
+ year = 100 * int(time.gmtime()[0] / 100) + int(year)
+ else:
+ year = int(year)
+ month = params.get('month', '-')
+ if not month or month == '-':
+ # ordinals are NOT normalized by mktime, we simulate them
+ # by setting month=1, day=ordinal
+ if ordinal:
+ month = 1
+ else:
+ month = time.gmtime()[1]
+ month = int(month)
+ day = params.get('day', 0)
+ if not day:
+ # see above
+ if ordinal:
+ day = ordinal
+ elif params.get('century', 0) or \
+ params.get('year', 0) or params.get('month', 0):
+ day = 1
+ else:
+ day = time.gmtime()[2]
+ else:
+ day = int(day)
+ # special case of the century - is the first year of the 21st century
+ # 2000 or 2001 ? The debate goes on...
+ if 'century' in params.keys():
+ year = (int(params['century']) - 1) * 100 + 1
+ # in ISO 8601 most fields are optional
+ for field in ['hour', 'minute', 'second', 'tzhour', 'tzmin']:
+ if not params.get(field, None):
+ params[field] = 0
+ hour = int(params.get('hour', 0))
+ minute = int(params.get('minute', 0))
+ second = int(params.get('second', 0))
+ # weekday is normalized by mktime(), we can ignore it
+ weekday = 0
+ # daylight savings is complex, but not needed for feedparser's purposes
+ # as time zones, if specified, include mention of whether it is active
+ # (e.g. PST vs. PDT, CET). Using -1 is implementation-dependent and
+ # and most implementations have DST bugs
+ daylight_savings_flag = 0
+ tm = [year, month, day, hour, minute, second, weekday,
+ ordinal, daylight_savings_flag]
+ # ISO 8601 time zone adjustments
+ tz = params.get('tz')
+ if tz and tz != 'Z':
+ if tz[0] == '-':
+ tm[3] += int(params.get('tzhour', 0))
+ tm[4] += int(params.get('tzmin', 0))
+ elif tz[0] == '+':
+ tm[3] -= int(params.get('tzhour', 0))
+ tm[4] -= int(params.get('tzmin', 0))
+ else:
+ return None
+ # Python's time.mktime() is a wrapper around the ANSI C mktime(3c)
+ # which is guaranteed to normalize d/m/y/h/m/s.
+ # Many implementations have bugs, but we'll pretend they don't.
+ return time.localtime(time.mktime(tm))
+registerDateHandler(_parse_date_iso8601)
+
+# 8-bit date handling routines written by ytrewq1.
+_korean_year = u'\ub144' # b3e2 in euc-kr
+_korean_month = u'\uc6d4' # bff9 in euc-kr
+_korean_day = u'\uc77c' # c0cf in euc-kr
+_korean_am = u'\uc624\uc804' # bfc0 c0fc in euc-kr
+_korean_pm = u'\uc624\ud6c4' # bfc0 c8c4 in euc-kr
+
+_korean_onblog_date_re = \
+ re.compile('(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' % \
+ (_korean_year, _korean_month, _korean_day))
+_korean_nate_date_re = \
+ re.compile(u'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' % \
+ (_korean_am, _korean_pm))
+def _parse_date_onblog(dateString):
+ '''Parse a string according to the OnBlog 8-bit date format'''
+ m = _korean_onblog_date_re.match(dateString)
+ if not m: return
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
+ 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
+ 'zonediff': '+09:00'}
+ if _debug: sys.stderr.write('OnBlog date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_onblog)
+
+def _parse_date_nate(dateString):
+ '''Parse a string according to the Nate 8-bit date format'''
+ m = _korean_nate_date_re.match(dateString)
+ if not m: return
+ hour = int(m.group(5))
+ ampm = m.group(4)
+ if (ampm == _korean_pm):
+ hour += 12
+ hour = str(hour)
+ if len(hour) == 1:
+ hour = '0' + hour
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
+ 'hour': hour, 'minute': m.group(6), 'second': m.group(7),\
+ 'zonediff': '+09:00'}
+ if _debug: sys.stderr.write('Nate date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_nate)
+
+_mssql_date_re = \
+ re.compile('(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(\.\d+)?')
+def _parse_date_mssql(dateString):
+ '''Parse a string according to the MS SQL date format'''
+ m = _mssql_date_re.match(dateString)
+ if not m: return
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
+ 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
+ 'zonediff': '+09:00'}
+ if _debug: sys.stderr.write('MS SQL date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_mssql)
+
+# Unicode strings for Greek date strings
+_greek_months = \
+ { \
+ u'\u0399\u03b1\u03bd': u'Jan', # c9e1ed in iso-8859-7
+ u'\u03a6\u03b5\u03b2': u'Feb', # d6e5e2 in iso-8859-7
+ u'\u039c\u03ac\u03ce': u'Mar', # ccdcfe in iso-8859-7
+ u'\u039c\u03b1\u03ce': u'Mar', # cce1fe in iso-8859-7
+ u'\u0391\u03c0\u03c1': u'Apr', # c1f0f1 in iso-8859-7
+ u'\u039c\u03ac\u03b9': u'May', # ccdce9 in iso-8859-7
+ u'\u039c\u03b1\u03ca': u'May', # cce1fa in iso-8859-7
+ u'\u039c\u03b1\u03b9': u'May', # cce1e9 in iso-8859-7
+ u'\u0399\u03bf\u03cd\u03bd': u'Jun', # c9effded in iso-8859-7
+ u'\u0399\u03bf\u03bd': u'Jun', # c9efed in iso-8859-7
+ u'\u0399\u03bf\u03cd\u03bb': u'Jul', # c9effdeb in iso-8859-7
+ u'\u0399\u03bf\u03bb': u'Jul', # c9f9eb in iso-8859-7
+ u'\u0391\u03cd\u03b3': u'Aug', # c1fde3 in iso-8859-7
+ u'\u0391\u03c5\u03b3': u'Aug', # c1f5e3 in iso-8859-7
+ u'\u03a3\u03b5\u03c0': u'Sep', # d3e5f0 in iso-8859-7
+ u'\u039f\u03ba\u03c4': u'Oct', # cfeaf4 in iso-8859-7
+ u'\u039d\u03bf\u03ad': u'Nov', # cdefdd in iso-8859-7
+ u'\u039d\u03bf\u03b5': u'Nov', # cdefe5 in iso-8859-7
+ u'\u0394\u03b5\u03ba': u'Dec', # c4e5ea in iso-8859-7
+ }
+
+_greek_wdays = \
+ { \
+ u'\u039a\u03c5\u03c1': u'Sun', # caf5f1 in iso-8859-7
+ u'\u0394\u03b5\u03c5': u'Mon', # c4e5f5 in iso-8859-7
+ u'\u03a4\u03c1\u03b9': u'Tue', # d4f1e9 in iso-8859-7
+ u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7
+ u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7
+ u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7
+ u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7
+ }
+
+_greek_date_format_re = \
+ re.compile(u'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)')
+
+def _parse_date_greek(dateString):
+ '''Parse a string according to a Greek 8-bit date format.'''
+ m = _greek_date_format_re.match(dateString)
+ if not m: return
+ try:
+ wday = _greek_wdays[m.group(1)]
+ month = _greek_months[m.group(3)]
+ except:
+ return
+ rfc822date = '%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s' % \
+ {'wday': wday, 'day': m.group(2), 'month': month, 'year': m.group(4),\
+ 'hour': m.group(5), 'minute': m.group(6), 'second': m.group(7),\
+ 'zonediff': m.group(8)}
+ if _debug: sys.stderr.write('Greek date parsed as: %s\n' % rfc822date)
+ return _parse_date_rfc822(rfc822date)
+registerDateHandler(_parse_date_greek)
+
+# Unicode strings for Hungarian date strings
+_hungarian_months = \
+ { \
+ u'janu\u00e1r': u'01', # e1 in iso-8859-2
+ u'febru\u00e1ri': u'02', # e1 in iso-8859-2
+ u'm\u00e1rcius': u'03', # e1 in iso-8859-2
+ u'\u00e1prilis': u'04', # e1 in iso-8859-2
+ u'm\u00e1ujus': u'05', # e1 in iso-8859-2
+ u'j\u00fanius': u'06', # fa in iso-8859-2
+ u'j\u00falius': u'07', # fa in iso-8859-2
+ u'augusztus': u'08',
+ u'szeptember': u'09',
+ u'okt\u00f3ber': u'10', # f3 in iso-8859-2
+ u'november': u'11',
+ u'december': u'12',
+ }
+
+_hungarian_date_format_re = \
+ re.compile(u'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})((\+|-)(\d{,2}:\d{2}))')
+
+def _parse_date_hungarian(dateString):
+ '''Parse a string according to a Hungarian 8-bit date format.'''
+ m = _hungarian_date_format_re.match(dateString)
+ if not m: return
+ try:
+ month = _hungarian_months[m.group(2)]
+ day = m.group(3)
+ if len(day) == 1:
+ day = '0' + day
+ hour = m.group(4)
+ if len(hour) == 1:
+ hour = '0' + hour
+ except:
+ return
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': month, 'day': day,\
+ 'hour': hour, 'minute': m.group(5),\
+ 'zonediff': m.group(6)}
+ if _debug: sys.stderr.write('Hungarian date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_hungarian)
+
+# W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
+# Drake and licensed under the Python license. Removed all range checking
+# for month, day, hour, minute, and second, since mktime will normalize
+# these later
+def _parse_date_w3dtf(dateString):
+ def __extract_date(m):
+ year = int(m.group('year'))
+ if year < 100:
+ year = 100 * int(time.gmtime()[0] / 100) + int(year)
+ if year < 1000:
+ return 0, 0, 0
+ julian = m.group('julian')
+ if julian:
+ julian = int(julian)
+ month = julian / 30 + 1
+ day = julian % 30 + 1
+ jday = None
+ while jday != julian:
+ t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
+ jday = time.gmtime(t)[-2]
+ diff = abs(jday - julian)
+ if jday > julian:
+ if diff < day:
+ day = day - diff
+ else:
+ month = month - 1
+ day = 31
+ elif jday < julian:
+ if day + diff < 28:
+ day = day + diff
+ else:
+ month = month + 1
+ return year, month, day
+ month = m.group('month')
+ day = 1
+ if month is None:
+ month = 1
+ else:
+ month = int(month)
+ day = m.group('day')
+ if day:
+ day = int(day)
+ else:
+ day = 1
+ return year, month, day
+
+ def __extract_time(m):
+ if not m:
+ return 0, 0, 0
+ hours = m.group('hours')
+ if not hours:
+ return 0, 0, 0
+ hours = int(hours)
+ minutes = int(m.group('minutes'))
+ seconds = m.group('seconds')
+ if seconds:
+ seconds = int(seconds)
+ else:
+ seconds = 0
+ return hours, minutes, seconds
+
+ def __extract_tzd(m):
+ '''Return the Time Zone Designator as an offset in seconds from UTC.'''
+ if not m:
+ return 0
+ tzd = m.group('tzd')
+ if not tzd:
+ return 0
+ if tzd == 'Z':
+ return 0
+ hours = int(m.group('tzdhours'))
+ minutes = m.group('tzdminutes')
+ if minutes:
+ minutes = int(minutes)
+ else:
+ minutes = 0
+ offset = (hours*60 + minutes) * 60
+ if tzd[0] == '+':
+ return -offset
+ return offset
+
+ __date_re = ('(?P\d\d\d\d)'
+ '(?:(?P-|)'
+ '(?:(?P\d\d\d)'
+ '|(?P\d\d)(?:(?P=dsep)(?P\d\d))?))?')
+ __tzd_re = '(?P[-+](?P\d\d)(?::?(?P\d\d))|Z)'
+ __tzd_rx = re.compile(__tzd_re)
+ __time_re = ('(?P\d\d)(?P:|)(?P\d\d)'
+ '(?:(?P=tsep)(?P\d\d(?:[.,]\d+)?))?'
+ + __tzd_re)
+ __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
+ __datetime_rx = re.compile(__datetime_re)
+ m = __datetime_rx.match(dateString)
+ if (m is None) or (m.group() != dateString): return
+ gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0)
+ if gmt[0] == 0: return
+ return time.gmtime(time.mktime(gmt) + __extract_tzd(m) - time.timezone)
+registerDateHandler(_parse_date_w3dtf)
+
+def _parse_date_rfc822(dateString):
+ '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
+ data = dateString.split()
+ if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames:
+ del data[0]
+ if len(data) == 4:
+ s = data[3]
+ i = s.find('+')
+ if i > 0:
+ data[3:] = [s[:i], s[i+1:]]
+ else:
+ data.append('')
+ dateString = " ".join(data)
+ if len(data) < 5:
+ dateString += ' 00:00:00 GMT'
+ tm = rfc822.parsedate_tz(dateString)
+ if tm:
+ return time.gmtime(rfc822.mktime_tz(tm))
+# rfc822.py defines several time zones, but we define some extra ones.
+# 'ET' is equivalent to 'EST', etc.
+_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
+rfc822._timezones.update(_additional_timezones)
+registerDateHandler(_parse_date_rfc822)
+
+def _parse_date(dateString):
+ '''Parses a variety of date formats into a 9-tuple in GMT'''
+ for handler in _date_handlers:
+ try:
+ date9tuple = handler(dateString)
+ if not date9tuple: continue
+ if len(date9tuple) != 9:
+ if _debug: sys.stderr.write('date handler function must return 9-tuple\n')
+ raise ValueError
+ map(int, date9tuple)
+ return date9tuple
+ except Exception, e:
+ if _debug: sys.stderr.write('%s raised %s\n' % (handler.__name__, repr(e)))
+ pass
+ return None
+
+def _getCharacterEncoding(http_headers, xml_data):
+ '''Get the character encoding of the XML document
+
+ http_headers is a dictionary
+ xml_data is a raw string (not Unicode)
+
+ This is so much trickier than it sounds, it's not even funny.
+ According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type
+ is application/xml, application/*+xml,
+ application/xml-external-parsed-entity, or application/xml-dtd,
+ the encoding given in the charset parameter of the HTTP Content-Type
+ takes precedence over the encoding given in the XML prefix within the
+ document, and defaults to 'utf-8' if neither are specified. But, if
+ the HTTP Content-Type is text/xml, text/*+xml, or
+ text/xml-external-parsed-entity, the encoding given in the XML prefix
+ within the document is ALWAYS IGNORED and only the encoding given in
+ the charset parameter of the HTTP Content-Type header should be
+ respected, and it defaults to 'us-ascii' if not specified.
+
+ Furthermore, discussion on the atom-syntax mailing list with the
+ author of RFC 3023 leads me to the conclusion that any document
+ served with a Content-Type of text/* and no charset parameter
+ must be treated as us-ascii. (We now do this.) And also that it
+ must always be flagged as non-well-formed. (We now do this too.)
+
+ If Content-Type is unspecified (input was local file or non-HTTP source)
+ or unrecognized (server just got it totally wrong), then go by the
+ encoding given in the XML prefix of the document and default to
+ 'iso-8859-1' as per the HTTP specification (RFC 2616).
+
+ Then, assuming we didn't find a character encoding in the HTTP headers
+ (and the HTTP Content-type allowed us to look in the body), we need
+ to sniff the first few bytes of the XML data and try to determine
+ whether the encoding is ASCII-compatible. Section F of the XML
+ specification shows the way here:
+ http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
+
+ If the sniffed encoding is not ASCII-compatible, we need to make it
+ ASCII compatible so that we can sniff further into the XML declaration
+ to find the encoding attribute, which will tell us the true encoding.
+
+ Of course, none of this guarantees that we will be able to parse the
+ feed in the declared character encoding (assuming it was declared
+ correctly, which many are not). CJKCodecs and iconv_codec help a lot;
+ you should definitely install them if you can.
+ http://cjkpython.i18n.org/
+ '''
+
+ def _parseHTTPContentType(content_type):
+ '''takes HTTP Content-Type header and returns (content type, charset)
+
+ If no charset is specified, returns (content type, '')
+ If no content type is specified, returns ('', '')
+ Both return parameters are guaranteed to be lowercase strings
+ '''
+ content_type = content_type or ''
+ content_type, params = cgi.parse_header(content_type)
+ return content_type, params.get('charset', '').replace("'", '')
+
+ sniffed_xml_encoding = ''
+ xml_encoding = ''
+ true_encoding = ''
+ http_content_type, http_encoding = _parseHTTPContentType(http_headers.get('content-type'))
+ # Must sniff for non-ASCII-compatible character encodings before
+ # searching for XML declaration. This heuristic is defined in
+ # section F of the XML specification:
+ # http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
+ try:
+ if xml_data[:4] == '\x4c\x6f\xa7\x94':
+ # EBCDIC
+ xml_data = _ebcdic_to_ascii(xml_data)
+ elif xml_data[:4] == '\x00\x3c\x00\x3f':
+ # UTF-16BE
+ sniffed_xml_encoding = 'utf-16be'
+ xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
+ elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') and (xml_data[2:4] != '\x00\x00'):
+ # UTF-16BE with BOM
+ sniffed_xml_encoding = 'utf-16be'
+ xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
+ elif xml_data[:4] == '\x3c\x00\x3f\x00':
+ # UTF-16LE
+ sniffed_xml_encoding = 'utf-16le'
+ xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
+ elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and (xml_data[2:4] != '\x00\x00'):
+ # UTF-16LE with BOM
+ sniffed_xml_encoding = 'utf-16le'
+ xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
+ elif xml_data[:4] == '\x00\x00\x00\x3c':
+ # UTF-32BE
+ sniffed_xml_encoding = 'utf-32be'
+ xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
+ elif xml_data[:4] == '\x3c\x00\x00\x00':
+ # UTF-32LE
+ sniffed_xml_encoding = 'utf-32le'
+ xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
+ elif xml_data[:4] == '\x00\x00\xfe\xff':
+ # UTF-32BE with BOM
+ sniffed_xml_encoding = 'utf-32be'
+ xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
+ elif xml_data[:4] == '\xff\xfe\x00\x00':
+ # UTF-32LE with BOM
+ sniffed_xml_encoding = 'utf-32le'
+ xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
+ elif xml_data[:3] == '\xef\xbb\xbf':
+ # UTF-8 with BOM
+ sniffed_xml_encoding = 'utf-8'
+ xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
+ else:
+ # ASCII-compatible
+ pass
+ xml_encoding_match = re.compile('^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
+ except:
+ xml_encoding_match = None
+ if xml_encoding_match:
+ xml_encoding = xml_encoding_match.groups()[0].lower()
+ if sniffed_xml_encoding and (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', 'iso-10646-ucs-4', 'ucs-4', 'csucs4', 'utf-16', 'utf-32', 'utf_16', 'utf_32', 'utf16', 'u16')):
+ xml_encoding = sniffed_xml_encoding
+ acceptable_content_type = 0
+ application_content_types = ('application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity')
+ text_content_types = ('text/xml', 'text/xml-external-parsed-entity')
+ if (http_content_type in application_content_types) or \
+ (http_content_type.startswith('application/') and http_content_type.endswith('+xml')):
+ acceptable_content_type = 1
+ true_encoding = http_encoding or xml_encoding or 'utf-8'
+ elif (http_content_type in text_content_types) or \
+ (http_content_type.startswith('text/')) and http_content_type.endswith('+xml'):
+ acceptable_content_type = 1
+ true_encoding = http_encoding or 'us-ascii'
+ elif http_content_type.startswith('text/'):
+ true_encoding = http_encoding or 'us-ascii'
+ elif http_headers and (not http_headers.has_key('content-type')):
+ true_encoding = xml_encoding or 'iso-8859-1'
+ else:
+ true_encoding = xml_encoding or 'utf-8'
+ return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type
+
+def _toUTF8(data, encoding):
+ '''Changes an XML data stream on the fly to specify a new encoding
+
+ data is a raw sequence of bytes (not Unicode) that is presumed to be in %encoding already
+ encoding is a string recognized by encodings.aliases
+ '''
+ if _debug: sys.stderr.write('entering _toUTF8, trying encoding %s\n' % encoding)
+ # strip Byte Order Mark (if present)
+ if (len(data) >= 4) and (data[:2] == '\xfe\xff') and (data[2:4] != '\x00\x00'):
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-16be':
+ sys.stderr.write('trying utf-16be instead\n')
+ encoding = 'utf-16be'
+ data = data[2:]
+ elif (len(data) >= 4) and (data[:2] == '\xff\xfe') and (data[2:4] != '\x00\x00'):
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-16le':
+ sys.stderr.write('trying utf-16le instead\n')
+ encoding = 'utf-16le'
+ data = data[2:]
+ elif data[:3] == '\xef\xbb\xbf':
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-8':
+ sys.stderr.write('trying utf-8 instead\n')
+ encoding = 'utf-8'
+ data = data[3:]
+ elif data[:4] == '\x00\x00\xfe\xff':
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-32be':
+ sys.stderr.write('trying utf-32be instead\n')
+ encoding = 'utf-32be'
+ data = data[4:]
+ elif data[:4] == '\xff\xfe\x00\x00':
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-32le':
+ sys.stderr.write('trying utf-32le instead\n')
+ encoding = 'utf-32le'
+ data = data[4:]
+ newdata = unicode(data, encoding)
+ if _debug: sys.stderr.write('successfully converted %s data to unicode\n' % encoding)
+ declmatch = re.compile('^<\?xml[^>]*?>')
+ newdecl = ''''''
+ if declmatch.search(newdata):
+ newdata = declmatch.sub(newdecl, newdata)
+ else:
+ newdata = newdecl + u'\n' + newdata
+ return newdata.encode('utf-8')
+
+def _stripDoctype(data):
+ '''Strips DOCTYPE from XML document, returns (rss_version, stripped_data)
+
+ rss_version may be 'rss091n' or None
+ stripped_data is the same XML document, minus the DOCTYPE
+ '''
+ entity_pattern = re.compile(r']*?)>', re.MULTILINE)
+ data = entity_pattern.sub('', data)
+ doctype_pattern = re.compile(r']*?)>', re.MULTILINE)
+ doctype_results = doctype_pattern.findall(data)
+ doctype = doctype_results and doctype_results[0] or ''
+ if doctype.lower().count('netscape'):
+ version = 'rss091n'
+ else:
+ version = None
+ data = doctype_pattern.sub('', data)
+ return version, data
+
+def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[]):
+ '''Parse a feed from a URL, file, stream, or string'''
+ result = FeedParserDict()
+ result['feed'] = FeedParserDict()
+ result['entries'] = []
+ if _XML_AVAILABLE:
+ result['bozo'] = 0
+ if type(handlers) == types.InstanceType:
+ handlers = [handlers]
+ try:
+ f = _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers)
+ data = f.read()
+ except Exception, e:
+ result['bozo'] = 1
+ result['bozo_exception'] = e
+ data = ''
+ f = None
+
+ # if feed is gzip-compressed, decompress it
+ if f and data and hasattr(f, 'headers'):
+ if gzip and f.headers.get('content-encoding', '') == 'gzip':
+ try:
+ data = gzip.GzipFile(fileobj=_StringIO(data)).read()
+ except Exception, e:
+ # Some feeds claim to be gzipped but they're not, so
+ # we get garbage. Ideally, we should re-request the
+ # feed without the 'Accept-encoding: gzip' header,
+ # but we don't.
+ result['bozo'] = 1
+ result['bozo_exception'] = e
+ data = ''
+ elif zlib and f.headers.get('content-encoding', '') == 'deflate':
+ try:
+ data = zlib.decompress(data, -zlib.MAX_WBITS)
+ except Exception, e:
+ result['bozo'] = 1
+ result['bozo_exception'] = e
+ data = ''
+
+ # save HTTP headers
+ if hasattr(f, 'info'):
+ info = f.info()
+ result['etag'] = info.getheader('ETag')
+ last_modified = info.getheader('Last-Modified')
+ if last_modified:
+ result['modified'] = _parse_date(last_modified)
+ if hasattr(f, 'url'):
+ result['href'] = f.url
+ result['status'] = 200
+ if hasattr(f, 'status'):
+ result['status'] = f.status
+ if hasattr(f, 'headers'):
+ result['headers'] = f.headers.dict
+ if hasattr(f, 'close'):
+ f.close()
+
+ # there are four encodings to keep track of:
+ # - http_encoding is the encoding declared in the Content-Type HTTP header
+ # - xml_encoding is the encoding declared in the ; changed
+# project name
+#2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree);
+# removed unnecessary urllib code -- urllib2 should always be available anyway;
+# return actual url, status, and full HTTP headers (as result['url'],
+# result['status'], and result['headers']) if parsing a remote feed over HTTP --
+# this should pass all the HTTP tests at ;
+# added the latest namespace-of-the-week for RSS 2.0
+#2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom
+# User-Agent (otherwise urllib2 sends two, which confuses some servers)
+#2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for
+# inline and as used in some RSS 2.0 feeds
+#2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or
+# textInput, and also to return the character encoding (if specified)
+#2.6 - 1/1/2004 - MAP - dc:author support (MarekK); fixed bug tracking
+# nested divs within content (JohnD); fixed missing sys import (JohanS);
+# fixed regular expression to capture XML character encoding (Andrei);
+# added support for Atom 0.3-style links; fixed bug with textInput tracking;
+# added support for cloud (MartijnP); added support for multiple
+# category/dc:subject (MartijnP); normalize content model: 'description' gets
+# description (which can come from description, summary, or full content if no
+# description), 'content' gets dict of base/language/type/value (which can come
+# from content:encoded, xhtml:body, content, or fullitem);
+# fixed bug matching arbitrary Userland namespaces; added xml:base and xml:lang
+# tracking; fixed bug tracking unknown tags; fixed bug tracking content when
+# element is not in default namespace (like Pocketsoap feed);
+# resolve relative URLs in link, guid, docs, url, comments, wfw:comment,
+# wfw:commentRSS; resolve relative URLs within embedded HTML markup in
+# description, xhtml:body, content, content:encoded, title, subtitle,
+# summary, info, tagline, and copyright; added support for pingback and
+# trackback namespaces
+#2.7 - 1/5/2004 - MAP - really added support for trackback and pingback
+# namespaces, as opposed to 2.6 when I said I did but didn't really;
+# sanitize HTML markup within some elements; added mxTidy support (if
+# installed) to tidy HTML markup within some elements; fixed indentation
+# bug in _parse_date (FazalM); use socket.setdefaulttimeout if available
+# (FazalM); universal date parsing and normalization (FazalM): 'created', modified',
+# 'issued' are parsed into 9-tuple date format and stored in 'created_parsed',
+# 'modified_parsed', and 'issued_parsed'; 'date' is duplicated in 'modified'
+# and vice-versa; 'date_parsed' is duplicated in 'modified_parsed' and vice-versa
+#2.7.1 - 1/9/2004 - MAP - fixed bug handling " and '. fixed memory
+# leak not closing url opener (JohnD); added dc:publisher support (MarekK);
+# added admin:errorReportsTo support (MarekK); Python 2.1 dict support (MarekK)
+#2.7.4 - 1/14/2004 - MAP - added workaround for improperly formed tags in
+# encoded HTML (skadz); fixed unicode handling in normalize_attrs (ChrisL);
+# fixed relative URI processing for guid (skadz); added ICBM support; added
+# base64 support
+#2.7.5 - 1/15/2004 - MAP - added workaround for malformed DOCTYPE (seen on many
+# blogspot.com sites); added _debug variable
+#2.7.6 - 1/16/2004 - MAP - fixed bug with StringIO importing
+#3.0b3 - 1/23/2004 - MAP - parse entire feed with real XML parser (if available);
+# added several new supported namespaces; fixed bug tracking naked markup in
+# description; added support for enclosure; added support for source; re-added
+# support for cloud which got dropped somehow; added support for expirationDate
+#3.0b4 - 1/26/2004 - MAP - fixed xml:lang inheritance; fixed multiple bugs tracking
+# xml:base URI, one for documents that don't define one explicitly and one for
+# documents that define an outer and an inner xml:base that goes out of scope
+# before the end of the document
+#3.0b5 - 1/26/2004 - MAP - fixed bug parsing multiple links at feed level
+#3.0b6 - 1/27/2004 - MAP - added feed type and version detection, result['version']
+# will be one of SUPPORTED_VERSIONS.keys() or empty string if unrecognized;
+# added support for creativeCommons:license and cc:license; added support for
+# full Atom content model in title, tagline, info, copyright, summary; fixed bug
+# with gzip encoding (not always telling server we support it when we do)
+#3.0b7 - 1/28/2004 - MAP - support Atom-style author element in author_detail
+# (dictionary of 'name', 'url', 'email'); map author to author_detail if author
+# contains name + email address
+#3.0b8 - 1/28/2004 - MAP - added support for contributor
+#3.0b9 - 1/29/2004 - MAP - fixed check for presence of dict function; added
+# support for summary
+#3.0b10 - 1/31/2004 - MAP - incorporated ISO-8601 date parsing routines from
+# xml.util.iso8601
+#3.0b11 - 2/2/2004 - MAP - added 'rights' to list of elements that can contain
+# dangerous markup; fiddled with decodeEntities (not right); liberalized
+# date parsing even further
+#3.0b12 - 2/6/2004 - MAP - fiddled with decodeEntities (still not right);
+# added support to Atom 0.2 subtitle; added support for Atom content model
+# in copyright; better sanitizing of dangerous HTML elements with end tags
+# (script, frameset)
+#3.0b13 - 2/8/2004 - MAP - better handling of empty HTML tags (br, hr, img,
+# etc.) in embedded markup, in either HTML or XHTML form ( , , )
+#3.0b14 - 2/8/2004 - MAP - fixed CDATA handling in non-wellformed feeds under
+# Python 2.1
+#3.0b15 - 2/11/2004 - MAP - fixed bug resolving relative links in wfw:commentRSS;
+# fixed bug capturing author and contributor URL; fixed bug resolving relative
+# links in author and contributor URL; fixed bug resolvin relative links in
+# generator URL; added support for recognizing RSS 1.0; passed Simon Fell's
+# namespace tests, and included them permanently in the test suite with his
+# permission; fixed namespace handling under Python 2.1
+#3.0b16 - 2/12/2004 - MAP - fixed support for RSS 0.90 (broken in b15)
+#3.0b17 - 2/13/2004 - MAP - determine character encoding as per RFC 3023
+#3.0b18 - 2/17/2004 - MAP - always map description to summary_detail (Andrei);
+# use libxml2 (if available)
+#3.0b19 - 3/15/2004 - MAP - fixed bug exploding author information when author
+# name was in parentheses; removed ultra-problematic mxTidy support; patch to
+# workaround crash in PyXML/expat when encountering invalid entities
+# (MarkMoraes); support for textinput/textInput
+#3.0b20 - 4/7/2004 - MAP - added CDF support
+#3.0b21 - 4/14/2004 - MAP - added Hot RSS support
+#3.0b22 - 4/19/2004 - MAP - changed 'channel' to 'feed', 'item' to 'entries' in
+# results dict; changed results dict to allow getting values with results.key
+# as well as results[key]; work around embedded illformed HTML with half
+# a DOCTYPE; work around malformed Content-Type header; if character encoding
+# is wrong, try several common ones before falling back to regexes (if this
+# works, bozo_exception is set to CharacterEncodingOverride); fixed character
+# encoding issues in BaseHTMLProcessor by tracking encoding and converting
+# from Unicode to raw strings before feeding data to sgmllib.SGMLParser;
+# convert each value in results to Unicode (if possible), even if using
+# regex-based parsing
+#3.0b23 - 4/21/2004 - MAP - fixed UnicodeDecodeError for feeds that contain
+# high-bit characters in attributes in embedded HTML in description (thanks
+# Thijs van de Vossen); moved guid, date, and date_parsed to mapped keys in
+# FeedParserDict; tweaked FeedParserDict.has_key to return True if asking
+# about a mapped key
+#3.0fc1 - 4/23/2004 - MAP - made results.entries[0].links[0] and
+# results.entries[0].enclosures[0] into FeedParserDict; fixed typo that could
+# cause the same encoding to be tried twice (even if it failed the first time);
+# fixed DOCTYPE stripping when DOCTYPE contained entity declarations;
+# better textinput and image tracking in illformed RSS 1.0 feeds
+#3.0fc2 - 5/10/2004 - MAP - added and passed Sam's amp tests; added and passed
+# my blink tag tests
+#3.0fc3 - 6/18/2004 - MAP - fixed bug in _changeEncodingDeclaration that
+# failed to parse utf-16 encoded feeds; made source into a FeedParserDict;
+# duplicate admin:generatorAgent/@rdf:resource in generator_detail.url;
+# added support for image; refactored parse() fallback logic to try other
+# encodings if SAX parsing fails (previously it would only try other encodings
+# if re-encoding failed); remove unichr madness in normalize_attrs now that
+# we're properly tracking encoding in and out of BaseHTMLProcessor; set
+# feed.language from root-level xml:lang; set entry.id from rdf:about;
+# send Accept header
+#3.0 - 6/21/2004 - MAP - don't try iso-8859-1 (can't distinguish between
+# iso-8859-1 and windows-1252 anyway, and most incorrectly marked feeds are
+# windows-1252); fixed regression that could cause the same encoding to be
+# tried twice (even if it failed the first time)
+#3.0.1 - 6/22/2004 - MAP - default to us-ascii for all text/* content types;
+# recover from malformed content-type header parameter with no equals sign
+# ('text/xml; charset:iso-8859-1')
+#3.1 - 6/28/2004 - MAP - added and passed tests for converting HTML entities
+# to Unicode equivalents in illformed feeds (aaronsw); added and
+# passed tests for converting character entities to Unicode equivalents
+# in illformed feeds (aaronsw); test for valid parsers when setting
+# XML_AVAILABLE; make version and encoding available when server returns
+# a 304; add handlers parameter to pass arbitrary urllib2 handlers (like
+# digest auth or proxy support); add code to parse username/password
+# out of url and send as basic authentication; expose downloading-related
+# exceptions in bozo_exception (aaronsw); added __contains__ method to
+# FeedParserDict (aaronsw); added publisher_detail (aaronsw)
+#3.2 - 7/3/2004 - MAP - use cjkcodecs and iconv_codec if available; always
+# convert feed to UTF-8 before passing to XML parser; completely revamped
+# logic for determining character encoding and attempting XML parsing
+# (much faster); increased default timeout to 20 seconds; test for presence
+# of Location header on redirects; added tests for many alternate character
+# encodings; support various EBCDIC encodings; support UTF-16BE and
+# UTF16-LE with or without a BOM; support UTF-8 with a BOM; support
+# UTF-32BE and UTF-32LE with or without a BOM; fixed crashing bug if no
+# XML parsers are available; added support for 'Content-encoding: deflate';
+# send blank 'Accept-encoding: ' header if neither gzip nor zlib modules
+# are available
+#3.3 - 7/15/2004 - MAP - optimize EBCDIC to ASCII conversion; fix obscure
+# problem tracking xml:base and xml:lang if element declares it, child
+# doesn't, first grandchild redeclares it, and second grandchild doesn't;
+# refactored date parsing; defined public registerDateHandler so callers
+# can add support for additional date formats at runtime; added support
+# for OnBlog, Nate, MSSQL, Greek, and Hungarian dates (ytrewq1); added
+# zopeCompatibilityHack() which turns FeedParserDict into a regular
+# dictionary, required for Zope compatibility, and also makes command-
+# line debugging easier because pprint module formats real dictionaries
+# better than dictionary-like objects; added NonXMLContentType exception,
+# which is stored in bozo_exception when a feed is served with a non-XML
+# media type such as 'text/plain'; respect Content-Language as default
+# language if not xml:lang is present; cloud dict is now FeedParserDict;
+# generator dict is now FeedParserDict; better tracking of xml:lang,
+# including support for xml:lang='' to unset the current language;
+# recognize RSS 1.0 feeds even when RSS 1.0 namespace is not the default
+# namespace; don't overwrite final status on redirects (scenarios:
+# redirecting to a URL that returns 304, redirecting to a URL that
+# redirects to another URL with a different type of redirect); add
+# support for HTTP 303 redirects
+#4.0 - MAP - support for relative URIs in xml:base attribute; fixed
+# encoding issue with mxTidy (phopkins); preliminary support for RFC 3229;
+# support for Atom 1.0; support for iTunes extensions; new 'tags' for
+# categories/keywords/etc. as array of dict
+# {'term': term, 'scheme': scheme, 'label': label} to match Atom 1.0
+# terminology; parse RFC 822-style dates with no time; lots of other
+# bug fixes
+#4.1 - MAP - removed socket timeout; added support for chardet library
diff --git a/addons/wiki/web/widgets/templates/wiki.mako b/addons/wiki/web/widgets/templates/wiki.mako
new file mode 100644
index 00000000000..54976a8c937
--- /dev/null
+++ b/addons/wiki/web/widgets/templates/wiki.mako
@@ -0,0 +1,23 @@
+% if editable and not inline:
+
+
+% endif
+
+% if editable and inline:
+
+% endif
+
+% if editable and error:
+ ${error}
+% endif
+
+% if not editable and value:
+ ${data|n}
+% endif
+
diff --git a/addons/wiki/web/widgets/wiki.py b/addons/wiki/web/widgets/wiki.py
new file mode 100644
index 00000000000..61a81ca6260
--- /dev/null
+++ b/addons/wiki/web/widgets/wiki.py
@@ -0,0 +1,228 @@
+###############################################################################
+#
+# Copyright (C) 2007-TODAY Tiny ERP Pvt Ltd. All Rights Reserved.
+#
+# $Id$
+#
+# Developed by Tiny (http://openerp.com) and Axelor (http://axelor.com).
+#
+# The OpenERP web client is distributed under the "OpenERP Public License".
+# It's based on Mozilla Public License Version (MPL) 1.1 with following
+# restrictions:
+#
+# - All names, links and logos of Tiny, OpenERP and Axelor must be
+# kept as in original distribution without any changes in all software
+# screens, especially in start-up page and the software header, even if
+# the application source code has been changed or updated or code has been
+# added.
+#
+# - All distributions of the software must keep source code with OEPL.
+#
+# - All integrations to any other software must keep source code with OEPL.
+#
+# If you need commercial licence to remove this kind of restriction please
+# contact us.
+#
+# You can see the MPL licence at: http://www.mozilla.org/MPL/MPL-1.1.html
+#
+###############################################################################
+
+import re
+import random
+import locale
+
+from base64 import b64encode
+from base64 import b64decode
+from StringIO import StringIO
+
+import cherrypy
+import wikimarkup
+
+from openobject.widgets import JSLink, CSSLink
+
+from openerp.utils import rpc
+
+from openerp.widgets import register_widget
+from openerp.widgets.form import Text
+
+
+_image = re.compile(r'img:(.*)\.(.*)', re.UNICODE)
+_rss = re.compile(r'rss:(.*)\.(.*)', re.UNICODE)
+_attach = re.compile(r'attach:(.*)\.(.*)', re.UNICODE)
+_internalLinks = re.compile(r'\[\[.*\]\]', re.UNICODE)
+_edit = re.compile(r'edit:(.*)\|(.*)', re.UNICODE)
+_view = re.compile(r'view:(.*)\|(.*)', re.UNICODE)
+
+class WikiParser(wikimarkup.Parser):
+
+ def parse(self, text, id):
+ text = text.replace(' ', 'n-b-s-p')
+ text = text.replace('&', 'n-a-m-p')
+ text = text.replace('&','&')
+ text = text.replace('n-b-s-p', ' ')
+ text = text.replace('n-a-m-p', '&')
+ text = text.replace('', '')
+ text = text.replace(' ', '')
+
+ text = wikimarkup.to_unicode(text)
+ text = self.strip(text)
+
+ text = super(WikiParser, self).parse(text)
+ text = self.addImage(text, id)
+ text = self.attachDoc(text, id)
+ text = self.recordLink(text)
+ text = self.viewRecordLink(text)
+ text = self.addInternalLinks(text)
+ #TODO : already implemented but we will implement it later after releasing the 5.0
+ #text = self.addRss(text, id)
+ return text
+
+ def viewRecordLink(self, text):
+ def record(path):
+ record = path.group().replace('view:','').split("|")
+ model = record[0]
+ text = record[1].replace('\r','').strip()
+ label = "View Record"
+ if len(record) > 2:
+ label = record[2]
+ proxy = rpc.RPCProxy(model)
+ ids = proxy.name_search(text, [], 'ilike', {})
+ if len(ids):
+ id = ids[0][0]
+ else:
+ try:
+ id = int(text)
+ except:
+ id = 0
+ return "[[/openerp/form/view?model=%s&id=%d | %s]]" % (model, id, label)
+
+ bits = _view.sub(record, text)
+ return bits
+
+ def addRss(self, text, id):
+ def addrss(path):
+ rssurl = path.group().replace('rss:','')
+ import rss.feedparser as feedparser
+ data = feedparser.parse(rssurl)
+ values = "%s " % (data.feed.title)
+ values += "%s " % (data.channel.description)
+ for entry in data['entries']:
+ values += " " % (entry.link, entry.title)
+ values += "%s " % (entry.summary)
+
+ return values
+
+ bits = _rss.sub(addrss, text)
+ return bits
+
+ def attachDoc(self, text, id):
+ def document(path):
+ file = path.group().replace('attach:','')
+ if file.startswith('http') or file.startswith('ftp'):
+ return "Download File " % (file)
+ else:
+ proxy = rpc.RPCProxy('ir.attachment')
+ ids = proxy.search([('datas_fname','=',file.strip()), ('res_model','=','wiki.wiki'), ('res_id','=',id)])
+ if len(ids) > 0:
+ return "%s " % (file, id, file)
+ else:
+ return """Attach : %s """ % (id, file)
+ bits = _attach.sub(document, text)
+ return bits
+
+ def addImage(self, text, id):
+ def image(path):
+ file = path.group().replace('img:','')
+ if file.startswith('http') or file.startswith('ftp'):
+ return " " % (file)
+ else:
+ proxy = rpc.RPCProxy('ir.attachment')
+ ids = proxy.search([('datas_fname','=',file.strip()), ('res_model','=','wiki.wiki'), ('res_id','=',id)])
+ if len(ids) > 0:
+ return " " % (file, id)
+ else:
+ return """Attach : %s """ % (id, file)
+ #"[[/attachment/?model=wiki.wiki&id=%d | Attach:%s]]" % (id, file)
+ bits = _image.sub(image, text)
+ return bits
+
+ def recordLink(self, text):
+ def record(path):
+ record = path.group().replace('edit:','').split("|")
+ model = record[0]
+ text = record[1].replace('\r','').strip()
+ label = "Edit Record"
+ if len(record) > 2:
+ label = record[2]
+ proxy = rpc.RPCProxy(model)
+ ids = proxy.name_search(text, [], '=', {})
+ if len(ids):
+ id = ids[0][0]
+ else:
+ try:
+ id = int(text)
+ except:
+ id = 0
+ return "[[/openerp/form/edit?model=%s&id=%d | %s]]" % (model, id, label)
+
+ bits = _edit.sub(record, text)
+ return bits
+
+ def addInternalLinks(self, text):
+ proxy = rpc.RPCProxy('wiki.wiki')
+
+ def link(path):
+ link = path.group().replace('[','').replace('[','').replace(']','').replace(']','').split("|")
+ name_to_search = link[0].strip()
+ mids = proxy.search([('name','ilike', name_to_search)])
+ link_str = ""
+ if mids:
+ if len(link) == 2:
+ link_str = "%s " % (mids[0], link[1])
+ elif len(link) == 1:
+ link_str = "%s " % (mids[0], link[0])
+ else:
+ if len(link) == 2:
+ link_str = "%s " % (link[0], link[1])
+ elif len(link) == 1:
+ link_str = "%s " % (link[0])
+
+ return link_str
+
+ bits = _internalLinks.sub(link, text)
+ return bits
+
+def wiki2html(text, showToc, id):
+ p = WikiParser(show_toc=showToc)
+ return p.parse(text, id)
+
+class WikiWidget(Text):
+ template = "/wiki/widgets/templates/wiki.mako"
+
+ params = ["data"]
+
+ css = [CSSLink("wiki", "css/wiki.css")]
+
+ data = None
+
+ def set_value(self, value):
+ super(WikiWidget, self).set_value(value)
+
+ if value:
+ toc = True
+ id = False
+ if hasattr(cherrypy.request, 'terp_record'):
+ params = cherrypy.request.terp_params
+ if params._terp_model == 'wiki.wiki':
+ proxy = rpc.RPCProxy('wiki.wiki')
+ toc = proxy.read([params.id], ['toc'])[0]['toc']
+ id = params.id
+
+ text = value+'\n\n'
+ html = wiki2html(text, toc, id)
+
+ self.data = html
+
+register_widget(WikiWidget, ["text_wiki"])
diff --git a/addons/wiki/web/widgets/wikimarkup/__init__.py b/addons/wiki/web/widgets/wikimarkup/__init__.py
new file mode 100644
index 00000000000..91fec4bc4fd
--- /dev/null
+++ b/addons/wiki/web/widgets/wikimarkup/__init__.py
@@ -0,0 +1,2143 @@
+# coding: latin1
+"""
+MediaWiki-style markup
+
+Copyright (C) 2008 David Cramer
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+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, see .
+"""
+
+import re, random, locale
+from base64 import b64encode, b64decode
+
+# a few patterns we use later
+
+MW_COLON_STATE_TEXT = 0
+MW_COLON_STATE_TAG = 1
+MW_COLON_STATE_TAGSTART = 2
+MW_COLON_STATE_CLOSETAG = 3
+MW_COLON_STATE_TAGSLASH = 4
+MW_COLON_STATE_COMMENT = 5
+MW_COLON_STATE_COMMENTDASH = 6
+MW_COLON_STATE_COMMENTDASHDASH = 7
+
+_attributePat = re.compile(ur'''(?:^|\s)([A-Za-z0-9]+)(?:\s*=\s*(?:"([^<"]*)"|'([^<']*)'|([a-zA-Z0-9!#$%&()*,\-./:;<>?@[\]^_`{|}~]+)|#([0-9a-fA-F]+)))''', re.UNICODE)
+_space = re.compile(ur'\s+', re.UNICODE)
+_closePrePat = re.compile(u"]*?)(/?>)([^<]*)$', re.UNICODE)
+
+_htmlpairs = ( # Tags that must be closed
+ u'b', u'del', u'i', u'ins', u'u', u'font', u'big', u'small', u'sub', u'sup', u'h1',
+ u'h2', u'h3', u'h4', u'h5', u'h6', u'cite', u'code', u'em', u's',
+ u'strike', u'strong', u'tt', u'var', u'div', u'center',
+ u'blockquote', u'ol', u'ul', u'dl', u'table', u'caption', u'pre',
+ u'ruby', u'rt' , u'rb' , u'rp', u'p', u'span', u'u',
+)
+_htmlsingle = (
+ u'br', u'hr', u'li', u'dt', u'dd', u'img',
+)
+_htmlsingleonly = ( # Elements that cannot have close tags
+ u'br', u'hr', u'img',
+)
+_htmlnest = ( # Tags that can be nested--??
+ u'table', u'tr', u'td', u'th', u'div', u'blockquote', u'ol', u'ul',
+ u'dl', u'font', u'big', u'small', u'sub', u'sup', u'span', u'img',
+)
+_tabletags = ( # Can only appear inside table
+ u'td', u'th', u'tr',
+)
+_htmllist = ( # Tags used by list
+ u'ul', u'ol',
+)
+_listtags = ( # Tags that can appear in a list
+ u'li',
+)
+_htmlsingleallowed = _htmlsingle + _tabletags
+_htmlelements = _htmlsingle + _htmlpairs + _htmlnest
+
+_htmlEntities = {
+ u'Aacute': 193, u'aacute': 225, u'Acirc': 194, u'acirc': 226, u'acute': 180,
+ u'AElig': 198, u'aelig': 230, u'Agrave': 192, u'agrave': 224, u'alefsym': 8501,
+ u'Alpha': 913, u'alpha': 945, u'amp': 38, u'and': 8743, u'ang': 8736, u'Aring': 197,
+ u'aring': 229,
+ u'asymp': 8776,
+ u'Atilde': 195,
+ u'atilde': 227,
+ u'Auml': 196,
+ u'auml': 228,
+ u'bdquo': 8222,
+ u'Beta': 914,
+ u'beta': 946,
+ u'brvbar': 166,
+ u'bull': 8226,
+ u'cap': 8745,
+ u'Ccedil': 199,
+ u'ccedil': 231,
+ u'cedil': 184,
+ u'cent': 162,
+ u'Chi': 935,
+ u'chi': 967,
+ u'circ': 710,
+ u'clubs': 9827,
+ u'cong': 8773,
+ u'copy': 169,
+ u'crarr': 8629,
+ u'cup': 8746,
+ u'curren': 164,
+ u'dagger': 8224,
+ u'Dagger': 8225,
+ u'darr': 8595,
+ u'dArr': 8659,
+ u'deg': 176,
+ u'Delta': 916,
+ u'delta': 948,
+ u'diams': 9830,
+ u'divide': 247,
+ u'Eacute': 201,
+ u'eacute': 233,
+ u'Ecirc': 202,
+ u'ecirc': 234,
+ u'Egrave': 200,
+ u'egrave': 232,
+ u'empty': 8709,
+ u'emsp': 8195,
+ u'ensp': 8194,
+ u'Epsilon': 917,
+ u'epsilon': 949,
+ u'equiv': 8801,
+ u'Eta': 919,
+ u'eta': 951,
+ u'ETH': 208,
+ u'eth': 240,
+ u'Euml': 203,
+ u'euml': 235,
+ u'euro': 8364,
+ u'exist': 8707,
+ u'fnof': 402,
+ u'forall': 8704,
+ u'frac12': 189,
+ u'frac14': 188,
+ u'frac34': 190,
+ u'frasl': 8260,
+ u'Gamma': 915,
+ u'gamma': 947,
+ u'ge': 8805,
+ u'gt': 62,
+ u'harr': 8596,
+ u'hArr': 8660,
+ u'hearts': 9829,
+ u'hellip': 8230,
+ u'Iacute': 205,
+ u'iacute': 237,
+ u'Icirc': 206,
+ u'icirc': 238,
+ u'iexcl': 161,
+ u'Igrave': 204,
+ u'igrave': 236,
+ u'image': 8465,
+ u'infin': 8734,
+ u'int': 8747,
+ u'Iota': 921,
+ u'iota': 953,
+ u'iquest': 191,
+ u'isin': 8712,
+ u'Iuml': 207,
+ u'iuml': 239,
+ u'Kappa': 922,
+ u'kappa': 954,
+ u'Lambda': 923,
+ u'lambda': 955,
+ u'lang': 9001,
+ u'laquo': 171,
+ u'larr': 8592,
+ u'lArr': 8656,
+ u'lceil': 8968,
+ u'ldquo': 8220,
+ u'le': 8804,
+ u'lfloor': 8970,
+ u'lowast': 8727,
+ u'loz': 9674,
+ u'lrm': 8206,
+ u'lsaquo': 8249,
+ u'lsquo': 8216,
+ u'lt': 60,
+ u'macr': 175,
+ u'mdash': 8212,
+ u'micro': 181,
+ u'middot': 183,
+ u'minus': 8722,
+ u'Mu': 924,
+ u'mu': 956,
+ u'nabla': 8711,
+ u'nbsp': 160,
+ u'ndash': 8211,
+ u'ne': 8800,
+ u'ni': 8715,
+ u'not': 172,
+ u'notin': 8713,
+ u'nsub': 8836,
+ u'Ntilde': 209,
+ u'ntilde': 241,
+ u'Nu': 925,
+ u'nu': 957,
+ u'Oacute': 211,
+ u'oacute': 243,
+ u'Ocirc': 212,
+ u'ocirc': 244,
+ u'OElig': 338,
+ u'oelig': 339,
+ u'Ograve': 210,
+ u'ograve': 242,
+ u'oline': 8254,
+ u'Omega': 937,
+ u'omega': 969,
+ u'Omicron': 927,
+ u'omicron': 959,
+ u'oplus': 8853,
+ u'or': 8744,
+ u'ordf': 170,
+ u'ordm': 186,
+ u'Oslash': 216,
+ u'oslash': 248,
+ u'Otilde': 213,
+ u'otilde': 245,
+ u'otimes': 8855,
+ u'Ouml': 214,
+ u'ouml': 246,
+ u'para': 182,
+ u'part': 8706,
+ u'permil': 8240,
+ u'perp': 8869,
+ u'Phi': 934,
+ u'phi': 966,
+ u'Pi': 928,
+ u'pi': 960,
+ u'piv': 982,
+ u'plusmn': 177,
+ u'pound': 163,
+ u'prime': 8242,
+ u'Prime': 8243,
+ u'prod': 8719,
+ u'prop': 8733,
+ u'Psi': 936,
+ u'psi': 968,
+ u'quot': 34,
+ u'radic': 8730,
+ u'rang': 9002,
+ u'raquo': 187,
+ u'rarr': 8594,
+ u'rArr': 8658,
+ u'rceil': 8969,
+ u'rdquo': 8221,
+ u'real': 8476,
+ u'reg': 174,
+ u'rfloor': 8971,
+ u'Rho': 929,
+ u'rho': 961,
+ u'rlm': 8207,
+ u'rsaquo': 8250,
+ u'rsquo': 8217,
+ u'sbquo': 8218,
+ u'Scaron': 352,
+ u'scaron': 353,
+ u'sdot': 8901,
+ u'sect': 167,
+ u'shy': 173,
+ u'Sigma': 931,
+ u'sigma': 963,
+ u'sigmaf': 962,
+ u'sim': 8764,
+ u'spades': 9824,
+ u'sub': 8834,
+ u'sube': 8838,
+ u'sum': 8721,
+ u'sup': 8835,
+ u'sup1': 185,
+ u'sup2': 178,
+ u'sup3': 179,
+ u'supe': 8839,
+ u'szlig': 223,
+ u'Tau': 932,
+ u'tau': 964,
+ u'there4': 8756,
+ u'Theta': 920,
+ u'theta': 952,
+ u'thetasym': 977,
+ u'thinsp': 8201,
+ u'THORN': 222,
+ u'thorn': 254,
+ u'tilde': 732,
+ u'times': 215,
+ u'trade': 8482,
+ u'Uacute': 218,
+ u'uacute': 250,
+ u'uarr': 8593,
+ u'uArr': 8657,
+ u'Ucirc': 219,
+ u'ucirc': 251,
+ u'Ugrave': 217,
+ u'ugrave': 249,
+ u'uml': 168,
+ u'upsih': 978,
+ u'Upsilon': 933,
+ u'upsilon': 965,
+ u'Uuml': 220,
+ u'uuml': 252,
+ u'weierp': 8472,
+ u'Xi': 926,
+ u'xi': 958,
+ u'Yacute': 221,
+ u'yacute': 253,
+ u'yen': 165,
+ u'Yuml': 376,
+ u'yuml': 255,
+ u'Zeta': 918,
+ u'zeta': 950,
+ u'zwj': 8205,
+ u'zwnj': 8204
+}
+
+_charRefsPat = re.compile(ur'''(&([A-Za-z0-9]+);|([0-9]+);|[xX]([0-9A-Za-z]+);|(&))''', re.UNICODE)
+_cssCommentPat = re.compile(ur'''\*.*?\*''', re.UNICODE)
+_toUTFPat = re.compile(ur'''\\([0-9A-Fa-f]{1,6})[\s]?''', re.UNICODE)
+_hackPat = re.compile(ur'''(expression|tps*://|url\s*\().*''', re.UNICODE | re.IGNORECASE)
+_hrPat = re.compile(u'''^-----*''', re.UNICODE | re.MULTILINE)
+_h1Pat = re.compile(u'^=(.+)=\s*$', re.UNICODE | re.MULTILINE)
+_h2Pat = re.compile(u'^==(.+)==\s*$', re.UNICODE | re.MULTILINE)
+_h3Pat = re.compile(u'^===(.+)===\s*$', re.UNICODE | re.MULTILINE)
+_h4Pat = re.compile(u'^====(.+)====\s*$', re.UNICODE | re.MULTILINE)
+_h5Pat = re.compile(u'^=====(.+)=====\s*$', re.UNICODE | re.MULTILINE)
+_h6Pat = re.compile(u'^======(.+)======\s*$', re.UNICODE | re.MULTILINE)
+_quotePat = re.compile(u"""(''+)""", re.UNICODE)
+_removePat = re.compile(ur'\b(' + ur'|'.join((u"a", u"an", u"as", u"at", u"before", u"but", u"by", u"for", u"from",
+ u"is", u"in", u"into", u"like", u"of", u"off", u"on", u"onto", u"per",
+ u"since", u"than", u"the", u"this", u"that", u"to", u"up", u"via",
+ u"with")) + ur')\b', re.UNICODE | re.IGNORECASE)
+_nonWordSpaceDashPat = re.compile(ur'[^\w\s\-\./]', re.UNICODE)
+_multiSpacePat = re.compile(ur'[\s\-_\./]+', re.UNICODE)
+_spacePat = re.compile(ur' ', re.UNICODE)
+_linkPat = re.compile(ur'^(?:([A-Za-z0-9]+):)?([^\|]+)(?:\|([^\n]+?))?\]\](.*)$', re.UNICODE | re.DOTALL)
+_bracketedLinkPat = re.compile(ur'(?:\[((?:mailto:|irc://|https?://|ftp://|/)[^<>\]\[' + u"\x00-\x20\x7f" + ur']*)\s*(.*?)\])', re.UNICODE)
+_protocolPat = re.compile(ur'(\b(?:mailto:|irc://|https?://|ftp://))', re.UNICODE)
+_specialUrlPat = re.compile(ur'^([^<>\]\[' + u"\x00-\x20\x7f" + ur']+)(.*)$', re.UNICODE)
+_protocolsPat = re.compile(ur'^(mailto:|irc://|https?://|ftp://)$', re.UNICODE)
+_controlCharsPat = re.compile(ur'[\]\[<>"' + u"\\x00-\\x20\\x7F" + ur']]', re.UNICODE)
+_hostnamePat = re.compile(ur'^([^:]+:)(//[^/]+)?(.*)$', re.UNICODE)
+_stripPat = re.compile(u'\\s|\u00ad|\u1806|\u200b|\u2060|\ufeff|\u03f4|\u034f|\u180b|\u180c|\u180d|\u200c|\u200d|[\ufe00-\ufe0f]', re.UNICODE)
+_zomgPat = re.compile(ur'^(:*)\{\|(.*)$', re.UNICODE)
+_headerPat = re.compile(ur"<[Hh]([1-6])(.*?)>(.*?)[Hh][1-6] *>", re.UNICODE)
+_templateSectionPat = re.compile(ur"", re.UNICODE)
+_tagPat = re.compile(ur"<.*?>", re.UNICODE)
+_startRegexHash = {}
+_endRegexHash = {}
+_endCommentPat = re.compile(ur'(-->)', re.UNICODE)
+_extractTagsAndParams_n = 1
+_guillemetLeftPat = re.compile(ur'(.) (\?|:|;|!|\302\273)', re.UNICODE)
+_guillemetRightPat = re.compile(ur'(\302\253) ', re.UNICODE)
+
+def setupAttributeWhitelist():
+ common = ( u'id', u'class', u'lang', u'dir', u'title', u'style' )
+ block = common + (u'align',)
+ tablealign = ( u'align', u'char', u'charoff', u'valign' )
+ tablecell = ( u'abbr',
+ u'axis',
+ u'headers',
+ u'scope',
+ u'rowspan',
+ u'colspan',
+ u'nowrap', # deprecated
+ u'width', # deprecated
+ u'height', # deprecated
+ u'bgcolor' # deprecated
+ )
+ return {
+ u'div': block,
+ u'center': common, # deprecated
+ u'span': block, # ??
+ u'h1': block,
+ u'h2': block,
+ u'h3': block,
+ u'h4': block,
+ u'h5': block,
+ u'h6': block,
+ u'em': common,
+ u'strong': common,
+ u'cite': common,
+ u'code': common,
+ u'var': common,
+ u'img': common + (u'src', u'alt', u'width', u'height',),
+ u'blockquote': common + (u'cite',),
+ u'sub': common,
+ u'sup': common,
+ u'p': block,
+ u'br': (u'id', u'class', u'title', u'style', u'clear',),
+ u'pre': common + (u'width',),
+ u'ins': common + (u'cite', u'datetime'),
+ u'del': common + (u'cite', u'datetime'),
+ u'ul': common + (u'type',),
+ u'ol': common + (u'type', u'start'),
+ u'li': common + (u'type', u'value'),
+ u'dl': common,
+ u'dd': common,
+ u'dt': common,
+ u'table': common + ( u'summary', u'width', u'border', u'frame',
+ u'rules', u'cellspacing', u'cellpadding',
+ u'align', u'bgcolor',
+ ),
+ u'caption': common + (u'align',),
+ u'thead': common + tablealign,
+ u'tfoot': common + tablealign,
+ u'tbody': common + tablealign,
+ u'colgroup': common + ( u'span', u'width' ) + tablealign,
+ u'col': common + ( u'span', u'width' ) + tablealign,
+ u'tr': common + ( u'bgcolor', ) + tablealign,
+ u'td': common + tablecell + tablealign,
+ u'th': common + tablecell + tablealign,
+ u'tt': common,
+ u'b': common,
+ u'i': common,
+ u'big': common,
+ u'small': common,
+ u'strike': common,
+ u's': common,
+ u'u': common,
+ u'font': common + ( u'size', u'color', u'face' ),
+ u'hr': common + ( u'noshade', u'size', u'width' ),
+ u'ruby': common,
+ u'rb': common,
+ u'rt': common, #array_merge( $common, array( 'rbspan' ) ),
+ u'rp': common,
+ }
+_whitelist = setupAttributeWhitelist()
+_page_cache = {}
+env = {}
+
+def registerTagHook(tag, function):
+ mTagHooks[tag] = function
+
+class BaseParser(object):
+ def __init__(self):
+ self.uniq_prefix = u"\x07UNIQ" + unicode(random.randint(1, 1000000000))
+ self.strip_state = {}
+ self.arg_stack = []
+ self.env = env
+ self.keep_env = (env != {})
+
+ def __del__(self):
+ if not self.keep_env:
+ global env
+ env = {}
+
+ ''' Used to store objects in the environment
+ used to prevent recursive imports '''
+ def store_object(self, namespace, key, value=True):
+ # Store the item to not reprocess it
+ if namespace not in self.env:
+ self.env[namespace] = {}
+ self.env[namespace][key] = value
+
+ def has_object(self, namespace, key):
+ if namespace not in self.env:
+ self.env[namespace] = {}
+ if hasattr(self, 'count'):
+ data = self.env[namespace]
+ test = key in data
+ ls
+ self.count = True
+ return key in self.env[namespace]
+
+ def retrieve_object(self, namespace, key, default=None):
+ if not self.env.get(namespace):
+ self.env[namespace] = {}
+ return self.env[namespace].get(key, default)
+
+ def parse(self, text):
+ utf8 = isinstance(text, str)
+ text = to_unicode(text)
+ if text[-1:] != u'\n':
+ text = text + u'\n'
+ taggedNewline = True
+ else:
+ taggedNewline = False
+
+ text = self.strip(text)
+ text = self.removeHtmlTags(text)
+ text = self.parseHorizontalRule(text)
+ text = self.parseAllQuotes(text)
+ text = self.replaceExternalLinks(text)
+ text = self.unstrip(text)
+ text = self.fixtags(text)
+ text = self.doBlockLevels(text, True)
+ text = self.unstripNoWiki(text)
+ text = text.split(u'\n')
+ text = u'\n'.join(text)
+ if taggedNewline and text[-1:] == u'\n':
+ text = text[:-1]
+ if utf8:
+ return text.encode("utf-8")
+ return text
+
+ def strip(self, text, stripcomments=False, dontstrip=[]):
+ render = True
+
+ commentState = {}
+
+ elements = ['nowiki',] + mTagHooks.keys()
+ if True: #wgRawHtml
+ elements.append('html')
+
+ # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
+ for k in dontstrip:
+ if k in elements:
+ del elements[k]
+
+ matches = {}
+ text = self.extractTagsAndParams(elements, text, matches)
+
+ for marker in matches:
+ element, content, params, tag = matches[marker]
+ if render:
+ tagName = element.lower()
+ if tagName == u'!--':
+ # comment
+ output = tag
+ if tag[-3:] != u'-->':
+ output += "-->"
+ elif tagName == u'html':
+ output = content
+ elif tagName == u'nowiki':
+ output = content.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>')
+ else:
+ if tagName in mTagHooks:
+ output = mTagHooks[tagName](self, content, params)
+ else:
+ output = content.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>')
+ else:
+ # Just stripping tags; keep the source
+ output = tag
+
+ # Unstrip the output, because unstrip() is no longer recursive so
+ # it won't do it itself
+ output = self.unstrip(output)
+
+ if not stripcomments and element == u'!--':
+ commentState[marker] = output
+ elif element == u'html' or element == u'nowiki':
+ if 'nowiki' not in self.strip_state:
+ self.strip_state['nowiki'] = {}
+ self.strip_state['nowiki'][marker] = output
+ else:
+ if 'general' not in self.strip_state:
+ self.strip_state['general'] = {}
+ self.strip_state['general'][marker] = output
+
+ # Unstrip comments unless explicitly told otherwise.
+ # (The comments are always stripped prior to this point, so as to
+ # not invoke any extension tags / parser hooks contained within
+ # a comment.)
+ if not stripcomments:
+ # Put them all back and forget them
+ for k in commentState:
+ v = commentState[k]
+ text = text.replace(k, v)
+
+ return text
+
+ def removeHtmlTags(self, text):
+ """convert bad tags into HTML identities"""
+ sb = []
+ text = self.removeHtmlComments(text)
+ bits = text.split(u'<')
+ sb.append(bits.pop(0))
+ tagstack = []
+ tablestack = tagstack
+ for x in bits:
+ m = _tagPattern.match(x)
+ if not m:
+ continue
+ slash, t, params, brace, rest = m.groups()
+ t = t.lower()
+ badtag = False
+ if t in _htmlelements:
+ # Check our stack
+ if slash:
+ # Closing a tag...
+ if t in _htmlsingleonly or len(tagstack) == 0:
+ badtag = True
+ else:
+ ot = tagstack.pop()
+ if ot != t:
+ if ot in _htmlsingleallowed:
+ # Pop all elements with an optional close tag
+ # and see if we find a match below them
+ optstack = []
+ optstack.append(ot)
+ while True:
+ if len(tagstack) == 0:
+ break
+ ot = tagstack.pop()
+ if ot == t or ot not in _htmlsingleallowed:
+ break
+ optstack.append(ot)
+ if t != ot:
+ # No match. Push the optinal elements back again
+ badtag = True
+ tagstack += reversed(optstack)
+ else:
+ tagstack.append(ot)
+ # can be nested in or , skip those cases:
+ if ot not in _htmllist and t in _listtags:
+ badtag = True
+ elif t == u'table':
+ if len(tablestack) == 0:
+ bagtag = True
+ else:
+ tagstack = tablestack.pop()
+ newparams = u''
+ else:
+ # Keep track for later
+ if t in _tabletags and u'table' not in tagstack:
+ badtag = True
+ elif t in tagstack and t not in _htmlnest:
+ badtag = True
+ # Is it a self-closed htmlpair? (bug 5487)
+ elif brace == u'/>' and t in _htmlpairs:
+ badTag = True
+ elif t in _htmlsingleonly:
+ # Hack to force empty tag for uncloseable elements
+ brace = u'/>'
+ elif t in _htmlsingle:
+ # Hack to not close $htmlsingle tags
+ brace = None
+ else:
+ if t == u'table':
+ tablestack.append(tagstack)
+ tagstack = []
+ tagstack.append(t)
+ newparams = self.fixTagAttributes(params, t)
+ if not badtag:
+ rest = rest.replace(u'>', u'>')
+ if brace == u'/>':
+ close = u' /'
+ else:
+ close = u''
+ sb.append(u'<')
+ sb.append(slash)
+ sb.append(t)
+ sb.append(newparams)
+ sb.append(close)
+ sb.append(u'>')
+ sb.append(rest)
+ continue
+ sb.append(u'<')
+ sb.append(x.replace(u'>', u'>'))
+
+ # Close off any remaining tags
+ while tagstack:
+ t = tagstack.pop()
+ sb.append(u'')
+ sb.append(t)
+ sb.append(u'>\n')
+ if t == u'table':
+ if not tablestack:
+ break
+ tagstack = tablestack.pop()
+
+ return u''.join(sb)
+
+ def removeHtmlComments(self, text):
+ """remove comments from given text"""
+ sb = []
+ start = text.find(u'', start)
+ if end == -1:
+ break
+ end += 3
+
+ spaceStart = max(0, start-1)
+ spaceEnd = end
+ while text[spaceStart] == u' ' and spaceStart > 0:
+ spaceStart -= 1
+ while text[spaceEnd] == u' ':
+ spaceEnd += 1
+
+ if text[spaceStart] == u'\n' and text[spaceEnd] == u'\n':
+ sb.append(text[last:spaceStart])
+ sb.append(u'\n')
+ last = spaceEnd+1
+ else:
+ sb.append(text[last:spaceStart+1])
+ last = spaceEnd
+
+ start = text.find(u''
+
+ return result, mDTopen
+
+ def nextItem(self, char, mDTopen):
+ if char == u'*' or char == '#':
+ return u' ', None
+ elif char == u':' or char == u';':
+ close = u''
+ if mDTopen:
+ close = ''
+ if char == u';':
+ return close + u'', True
+ else:
+ return close + u' ', False
+ return u''
+
+ def closeList(self, char, mDTopen):
+ if char == u'*':
+ return u' \n'
+ elif char == u'#':
+ return u'\n'
+ elif char == u':':
+ if mDTopen:
+ return u'\n'
+ else:
+ return u'\n'
+ else:
+ return u''
+
+ def findColonNoLinks(self, text, before, after):
+ try:
+ pos = text.search(':')
+ except:
+ return False
+
+ lt = text.find('<')
+ if lt == -1 or lt > pos:
+ # Easy; no tag nesting to worry about
+ before = text[0:pos]
+ after = text[0:pos+1]
+ return before, after, pos
+
+ # Ugly state machine to walk through avoiding tags.
+ state = MW_COLON_STATE_TEXT;
+ stack = 0;
+ i = 0
+ while i < len(text):
+ c = text[i];
+
+ if state == 0: # MW_COLON_STATE_TEXT:
+ if text[i] == '<':
+ # Could be either a tag or an tag
+ state = MW_COLON_STATE_TAGSTART
+ elif text[i] == ':':
+ if stack == 0:
+ # we found it
+ return text[0:i], text[i+1], i
+ else:
+ # Skip ahead looking for something interesting
+ try:
+ colon = text.search(':', i)
+ except:
+ return False
+ lt = text.find('<', i)
+ if stack == 0:
+ if lt == -1 or colon < lt:
+ # we found it
+ return text[0:colon], text[colon+1], i
+ if lt == -1:
+ break
+ # Skip ahead to next tag start
+ i = lt
+ state = MW_COLON_STATE_TAGSTART
+ elif state == 1: # MW_COLON_STATE_TAG:
+ # In a
+ if text[i] == '>':
+ stack += 1
+ state = MW_COLON_STATE_TEXT
+ elif text[i] == '/':
+ state = MW_COLON_STATE_TAGSLASH
+ elif state == 2: # MW_COLON_STATE_TAGSTART:
+ if text[i] == '/':
+ state = MW_COLON_STATE_CLOSETAG
+ elif text[i] == '!':
+ state = MW_COLON_STATE_COMMENT
+ elif text[i] == '>':
+ # Illegal early close? This shouldn't happen D:
+ state = MW_COLON_STATE_TEXT
+ else:
+ state = MW_COLON_STATE_TAG
+ elif state == 3: # MW_COLON_STATE_CLOSETAG:
+ # In a
+ if text[i] == '>':
+ stack -= 1
+ if stack < 0:
+ return False
+ state = MW_COLON_STATE_TEXT
+ elif state == MW_COLON_STATE_TAGSLASH:
+ if text[i] == '>':
+ # Yes, a self-closed tag
+ state = MW_COLON_STATE_TEXT
+ else:
+ # Probably we're jumping the gun, and this is an attribute
+ state = MW_COLON_STATE_TAG
+ elif state == 5: # MW_COLON_STATE_COMMENT:
+ if text[i] == '-':
+ state = MW_COLON_STATE_COMMENTDASH
+ elif state == MW_COLON_STATE_COMMENTDASH:
+ if text[i] == '-':
+ state = MW_COLON_STATE_COMMENTDASHDASH
+ else:
+ state = MW_COLON_STATE_COMMENT
+ elif state == MW_COLON_STATE_COMMENTDASHDASH:
+ if text[i] == '>':
+ state = MW_COLON_STATE_TEXT
+ else:
+ state = MW_COLON_STATE_COMMENT
+ else:
+ raise
+ if stack > 0:
+ return False
+ return False
+
+ def doBlockLevels(self, text, linestart):
+ # Parsing through the text line by line. The main thing
+ # happening here is handling of block-level elements p, pre,
+ # and making lists from lines starting with * # : etc.
+ lastPrefix = u''
+ mDTopen = inBlockElem = False
+ prefixLength = 0
+ paragraphStack = False
+ _closeMatchPat = re.compile(ur"( 0:
+ tmpOutput, tmpMDTopen = self.nextItem(pref[commonPrefixLength-1], mDTopen)
+ output.append(tmpOutput)
+ if tmpMDTopen is not None:
+ mDTopen = tmpMDTopen
+
+ while prefixLength > commonPrefixLength:
+ char = pref[commonPrefixLength:commonPrefixLength+1]
+ tmpOutput, tmpMDTOpen = self.openList(char, mLastSection)
+ if tmpMDTOpen:
+ mDTopen = True
+ output.append(tmpOutput)
+ mLastSection = u''
+ mInPre = False
+
+ if char == u';':
+ # FIXME: This is dupe of code above
+ term = t2 = u''
+ z = self.findColonNoLinks(t, term, t2)
+ if z != False:
+ term, t2 = z[1:2]
+ t = t2
+ output.append(term)
+ tmpOutput, tmpMDTopen = self.nextItem(u':', mDTopen)
+ output.append(tmpOutput)
+ if tmpMDTopen is not None:
+ mDTopen = tmpMDTopen
+
+ commonPrefixLength += 1
+
+ lastPrefix = pref2
+
+ if prefixLength == 0:
+ # No prefix (not in list)--go to paragraph mode
+ # XXX: use a stack for nestable elements like span, table and div
+ openmatch = _openMatchPat.search(t)
+ closematch = _closeMatchPat.search(t)
+ if openmatch or closematch:
+ paragraphStack = False
+ output.append(self.closeParagraph(mLastSection))
+ mLastSection = u''
+ if preCloseMatch:
+ mInPre = False
+ if preOpenMatch:
+ mInPre = True
+ inBlockElem = bool(not closematch)
+ elif not inBlockElem and not mInPre:
+ if t[0:1] == u' ' and (mLastSection == u'pre' or t.strip() != u''):
+ # pre
+ if mLastSection != u'pre':
+ paragraphStack = False
+ output.append(self.closeParagraph(u'') + u'')
+ mInPre = False
+ mLastSection = u'pre'
+ t = t[1:]
+ else:
+ # paragraph
+ if t.strip() == u'':
+ if paragraphStack:
+ output.append(paragraphStack + u' ')
+ paragraphStack = False
+ mLastSection = u'p'
+ else:
+ if mLastSection != u'p':
+ output.append(self.closeParagraph(mLastSection))
+ mLastSection = u''
+ mInPre = False
+ paragraphStack = u''
+ else:
+ paragraphStack = u'
'
+ else:
+ if paragraphStack:
+ output.append(paragraphStack)
+ paragraphStack = False
+ mLastSection = u'p'
+ elif mLastSection != u'p':
+ output.append(self.closeParagraph(mLastSection) + u'
')
+ mLastSection = u'p'
+ mInPre = False
+
+ # somewhere above we forget to get out of pre block (bug 785)
+ if preCloseMatch and mInPre:
+ mInPre = False
+
+ if paragraphStack == False:
+ output.append(t + u"\n")
+
+ while prefixLength:
+ output.append(self.closeList(pref2[prefixLength-1], mDTopen))
+ mDTopen = False
+ prefixLength -= 1
+
+ if mLastSection != u'':
+ output.append(u'' + mLastSection + u'>')
+ mLastSection = u''
+
+ return ''.join(output)
+
+class Parser(BaseParser):
+ def __init__(self, show_toc=True):
+ super(Parser, self).__init__()
+ self.show_toc = show_toc
+
+ def parse(self, text):
+ utf8 = isinstance(text, str)
+ text = to_unicode(text)
+ if text[-1:] != u'\n':
+ text = text + u'\n'
+ taggedNewline = True
+ else:
+ taggedNewline = False
+
+ text = self.strip(text)
+ text = self.removeHtmlTags(text)
+ text = self.doTableStuff(text)
+ text = self.parseHorizontalRule(text)
+ text = self.checkTOC(text)
+ text = self.parseHeaders(text)
+ text = self.parseAllQuotes(text)
+ text = self.replaceExternalLinks(text)
+ if not self.show_toc and text.find(u"") == -1:
+ self.show_toc = False
+ text = self.formatHeadings(text, True)
+ text = self.unstrip(text)
+ text = self.fixtags(text)
+ text = self.doBlockLevels(text, True)
+ text = self.unstripNoWiki(text)
+ text = text.split(u'\n')
+ text = u'\n'.join(text)
+ if taggedNewline and text[-1:] == u'\n':
+ text = text[:-1]
+ if utf8:
+ return text.encode("utf-8")
+ return text
+
+ def checkTOC(self, text):
+ if text.find(u"__NOTOC__") != -1:
+ text = text.replace(u"__NOTOC__", u"")
+ self.show_toc = False
+ if text.find(u"__TOC__") != -1:
+ text = text.replace(u"__TOC__", u"")
+ self.show_toc = True
+ return text
+
+ def doTableStuff(self, text):
+ t = text.split(u"\n")
+ td = [] # Is currently a td tag open?
+ ltd = [] # Was it TD or TH?
+ tr = [] # Is currently a tr tag open?
+ ltr = [] # tr attributes
+ has_opened_tr = [] # Did this table open a
element?
+ indent_level = 0 # indent level of the table
+
+ for k, x in zip(range(len(t)), t):
+ x = x.strip()
+ fc = x[0:1]
+ matches = _zomgPat.match(x)
+ if matches:
+ indent_level = len(matches.group(1))
+
+ attributes = self.unstripForHTML(matches.group(2))
+
+ t[k] = u''*indent_level + u''
+ td.append(False)
+ ltd.append(u'')
+ tr.append(False)
+ ltr.append(u'')
+ has_opened_tr.append(False)
+ elif len(td) == 0:
+ pass
+ elif u'|}' == x[0:2]:
+ z = u"
" + x[2:]
+ l = ltd.pop()
+ if not has_opened_tr.pop():
+ z = u"" + z
+ if tr.pop():
+ z = u" " + z
+ if td.pop():
+ z = u'' + l + u'>' + z
+ ltr.pop()
+ t[k] = z + u' '*indent_level
+ elif u'|-' == x[0:2]: # Allows for |-------------
+ x = x[1:]
+ while x != u'' and x[0:1] == '-':
+ x = x[1:]
+ z = ''
+ l = ltd.pop()
+ has_opened_tr.pop()
+ has_opened_tr.append(True)
+ if tr.pop():
+ z = u' ' + z
+ if td.pop():
+ z = u'' + l + u'>' + z
+ ltr.pop()
+ t[k] = z
+ tr.append(False)
+ td.append(False)
+ ltd.append(u'')
+ attributes = self.unstripForHTML(x)
+ ltr.append(self.fixTagAttributes(attributes, u'tr'))
+ elif u'|' == fc or u'!' == fc or u'|+' == x[0:2]: # Caption
+ # x is a table row
+ if u'|+' == x[0:2]:
+ fc = u'+'
+ x = x[1:]
+ x = x[1:]
+ if fc == u'!':
+ x = x.replace(u'!!', u'||')
+ # Split up multiple cells on the same line.
+ # FIXME: This can result in improper nesting of tags processed
+ # by earlier parser steps, but should avoid splitting up eg
+ # attribute values containing literal "||".
+ x = x.split(u'||')
+
+ t[k] = u''
+
+ # Loop through each table cell
+ for theline in x:
+ z = ''
+ if fc != u'+':
+ tra = ltr.pop()
+ if not tr.pop():
+ z = u'\n'
+ tr.append(True)
+ ltr.append(u'')
+ has_opened_tr.pop()
+ has_opened_tr.append(True)
+ l = ltd.pop()
+ if td.pop():
+ z = u'' + l + u'>' + z
+ if fc == u'|':
+ l = u'td'
+ elif fc == u'!':
+ l = u'th'
+ elif fc == u'+':
+ l = u'caption'
+ else:
+ l = u''
+ ltd.append(l)
+
+ #Cell parameters
+ y = theline.split(u'|', 1)
+ # Note that a '|' inside an invalid link should not
+ # be mistaken as delimiting cell parameters
+ if y[0].find(u'[[') != -1:
+ y = [theline]
+
+ if len(y) == 1:
+ y = z + u"<" + l + u">" + y[0]
+ else:
+ attributes = self.unstripForHTML(y[0])
+ y = z + u"<" + l + self.fixTagAttributes(attributes, l) + u">" + y[1]
+
+ t[k] += y
+ td.append(True)
+
+ while len(td) > 0:
+ l = ltd.pop()
+ if td.pop():
+ t.append(u'')
+ if tr.pop():
+ t.append(u' ')
+ if not has_opened_tr.pop():
+ t.append(u' ')
+ t.append(u'')
+
+ text = u'\n'.join(t)
+ # special case: don't return empty table
+ if text == u" ":
+ text = u''
+
+ return text
+
+ def formatHeadings(self, text, isMain):
+ """
+ This function accomplishes several tasks:
+ 1) Auto-number headings if that option is enabled
+ 2) Add an [edit] link to sections for logged in users who have enabled the option
+ 3) Add a Table of contents on the top for users who have enabled the option
+ 4) Auto-anchor headings
+
+ It loops through all headlines, collects the necessary data, then splits up the
+ string and re-inserts the newly formatted headlines.
+ """
+ doNumberHeadings = False
+ showEditLink = True # Can User Edit
+
+ if text.find(u"__NOEDITSECTION__") != -1:
+ showEditLink = False
+ text = text.replace(u"__NOEDITSECTION__", u"")
+
+ # Get all headlines for numbering them and adding funky stuff like [edit]
+ # links - this is for later, but we need the number of headlines right now
+ matches = _headerPat.findall(text)
+ numMatches = len(matches)
+
+ # if there are fewer than 4 headlines in the article, do not show TOC
+ # unless it's been explicitly enabled.
+ enoughToc = self.show_toc and (numMatches >= 4 or text.find(u"") != -1)
+
+ # Allow user to stipulate that a page should have a "new section"
+ # link added via __NEWSECTIONLINK__
+ showNewSection = False
+ if text.find(u"__NEWSECTIONLINK__") != -1:
+ showNewSection = True
+ text = text.replace(u"__NEWSECTIONLINK__", u"")
+ # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
+ # override above conditions and always show TOC above first header
+ if text.find(u"__FORCETOC__") != -1:
+ self.show_toc = True
+ enoughToc = True
+ text = text.replace(u"__FORCETOC__", u"")
+ # Never ever show TOC if no headers
+ if numMatches < 1:
+ enoughToc = False
+
+ # headline counter
+ headlineCount = 0
+ sectionCount = 0 # headlineCount excluding template sections
+
+ # Ugh .. the TOC should have neat indentation levels which can be
+ # passed to the skin functions. These are determined here
+ toc = []
+ head = {}
+ sublevelCount = {}
+ levelCount = {}
+ toclevel = 0
+ level = 0
+ prevlevel = 0
+ toclevel = 0
+ prevtoclevel = 0
+ refers = {}
+ refcount = {}
+ wgMaxTocLevel = 5
+
+ for match in matches:
+ headline = match[2]
+ istemplate = False
+ templatetitle = u''
+ templatesection = 0
+ numbering = []
+
+ m = _templateSectionPat.search(headline)
+ if m:
+ istemplate = True
+ templatetitle = b64decode(m[0])
+ templatesection = 1 + int(b64decode(m[1]))
+ headline = _templateSectionPat.sub(u'', headline)
+
+ if toclevel:
+ prevlevel = level
+ prevtoclevel = toclevel
+
+ level = matches[headlineCount][0]
+
+ if doNumberHeadings or enoughToc:
+ if level > prevlevel:
+ toclevel += 1
+ sublevelCount[toclevel] = 0
+ if toclevel < wgMaxTocLevel:
+ toc.append(u'\n')
+ elif level < prevlevel and toclevel > 1:
+ # Decrease TOC level, find level to jump to
+
+ if toclevel == 2 and level < levelCount[1]:
+ toclevel = 1
+ else:
+ for i in range(toclevel, 0, -1):
+ if levelCount[i] == level:
+ # Found last matching level
+ toclevel = i
+ break
+ elif levelCount[i] < level:
+ toclevel = i + 1
+ break
+ if toclevel < wgMaxTocLevel:
+ toc.append(u"\n")
+ toc.append(u" \n\n" * max(prevtoclevel - toclevel, 0))
+ else:
+ if toclevel < wgMaxTocLevel:
+ toc.append(u"\n")
+
+ levelCount[toclevel] = level
+
+ # count number of headlines for each level
+ sublevelCount[toclevel] += 1
+ for i in range(1, toclevel+1):
+ if sublevelCount[i]:
+ numbering.append(to_unicode(sublevelCount[i]))
+
+ # The canonized header is a version of the header text safe to use for links
+ # Avoid insertion of weird stuff like by expanding the relevant sections
+ canonized_headline = self.unstrip(headline)
+ canonized_headline = self.unstripNoWiki(canonized_headline)
+
+ # -- don't know what to do with this yet.
+ # Remove link placeholders by the link text.
+ #
+ # turns into
+ # link text with suffix
+ # $canonized_headline = preg_replace( '//e',
+ # "\$this->mLinkHolders['texts'][\$1]",
+ # $canonized_headline );
+ # $canonized_headline = preg_replace( '//e',
+ # "\$this->mInterwikiLinkHolders['texts'][\$1]",
+ # $canonized_headline );
+
+ # strip out HTML
+ canonized_headline = _tagPat.sub(u'', canonized_headline)
+ tocline = canonized_headline.strip()
+ # Save headline for section edit hint before it's escaped
+ headline_hint = tocline
+ canonized_headline = self.escapeId(tocline)
+ refers[headlineCount] = canonized_headline
+
+ # count how many in assoc. array so we can track dupes in anchors
+ if canonized_headline not in refers:
+ refers[canonized_headline] = 1
+ else:
+ refers[canonized_headline] += 1
+ refcount[headlineCount] = refers[canonized_headline]
+
+ numbering = '.'.join(numbering)
+
+ # Don't number the heading if it is the only one (looks silly)
+ if doNumberHeadings and numMatches > 1:
+ # the two are different if the line contains a link
+ headline = numbering + u' ' + headline
+
+ # Create the anchor for linking from the TOC to the section
+ anchor = canonized_headline;
+ if refcount[headlineCount] > 1:
+ anchor += u'_' + unicode(refcount[headlineCount])
+
+ if enoughToc:
+ toc.append(u'\n')
+ toc.append(numbering)
+ toc.append(u' ')
+ toc.append(tocline)
+ toc.append(u' ')
+
+ # if showEditLink and (not istemplate or templatetitle != u""):
+ # if not head[headlineCount]:
+ # head[headlineCount] = u''
+ #
+ # if istemplate:
+ # head[headlineCount] += sk.editSectionLinkForOther(templatetile, templatesection)
+ # else:
+ # head[headlineCount] += sk.editSectionLink(mTitle, sectionCount+1, headline_hint)
+
+ # give headline the correct tag
+ if headlineCount not in head:
+ head[headlineCount] = []
+ h = head[headlineCount]
+ h.append(u'')
+ h.append(matches[headlineCount][1].strip())
+ h.append(headline.strip())
+ h.append(u' ')
+
+ headlineCount += 1
+
+ if not istemplate:
+ sectionCount += 1
+
+ if enoughToc:
+ if toclevel < wgMaxTocLevel:
+ toc.append(u" \n")
+ toc.append(u"\n\n" * max(0, toclevel - 1))
+ toc.insert(0, u'
' + _('Table of Contents') + ' ')
+ toc.append(u'\n')
+
+ # split up and insert constructed headlines
+
+ blocks = _headerPat.split(text)
+
+ i = 0
+ len_blocks = len(blocks)
+ forceTocPosition = text.find(u"")
+ full = []
+ while i < len_blocks:
+ j = i/4
+ full.append(blocks[i])
+ if enoughToc and not i and isMain and forceTocPosition == -1:
+ full += toc
+ toc = None
+ if j in head and head[j]:
+ full += head[j]
+ head[j] = None
+ i += 4
+ full = u''.join(full)
+ if forceTocPosition != -1:
+ return full.replace(u"", u''.join(toc), 1)
+ else:
+ return full
+
+def parse(text, showToc=True):
+ """Returns HTML from MediaWiki markup"""
+ p = Parser(show_toc=showToc)
+ return p.parse(text)
+
+def parselite(text):
+ """Returns HTML from MediaWiki markup ignoring
+ without headings"""
+ p = BaseParser()
+ return p.parse(text)
+
+def truncate_url(url, length=40):
+ if len(url) <= length:
+ return url
+ import re
+ pattern = r'(/[^/]+/?)$'
+ match = re.search(pattern, url)
+ if not match:
+ return url
+ l = len(match.group(1))
+ domain = url.replace(match.group(1), '')
+ firstpart = url[0:len(url)-l]
+ secondpart = match.group(1)
+ if firstpart == firstpart[0:length-3]:
+ secondpart = secondpart[0:length-3] + '...'
+ else:
+ firstpart = firstpart[0:length-3]
+ secondpart = '...' + secondpart
+ t_url = firstpart+secondpart
+ return t_url
+
+def to_unicode(text, charset=None):
+ """Convert a `str` object to an `unicode` object.
+
+ If `charset` is given, we simply assume that encoding for the text,
+ but we'll use the "replace" mode so that the decoding will always
+ succeed.
+ If `charset` is ''not'' specified, we'll make some guesses, first
+ trying the UTF-8 encoding, then trying the locale preferred encoding,
+ in "replace" mode. This differs from the `unicode` builtin, which
+ by default uses the locale preferred encoding, in 'strict' mode,
+ and is therefore prompt to raise `UnicodeDecodeError`s.
+
+ Because of the "replace" mode, the original content might be altered.
+ If this is not what is wanted, one could map the original byte content
+ by using an encoding which maps each byte of the input to an unicode
+ character, e.g. by doing `unicode(text, 'iso-8859-1')`.
+ """
+ if not isinstance(text, str):
+ if isinstance(text, Exception):
+ # two possibilities for storing unicode strings in exception data:
+ try:
+ # custom __str__ method on the exception (e.g. PermissionError)
+ return unicode(text)
+ except UnicodeError:
+ # unicode arguments given to the exception (e.g. parse_date)
+ return ' '.join([to_unicode(arg) for arg in text.args])
+ return unicode(text)
+ if charset:
+ return unicode(text, charset, 'replace')
+ else:
+ try:
+ return unicode(text, 'utf-8')
+ except UnicodeError:
+ return unicode(text, locale.getpreferredencoding(), 'replace')
+
+# tag hooks
+mTagHooks = {}
+
+## IMPORTANT
+## Make sure all hooks output CLEAN html. Escape any user input BEFORE it's returned
+
+# Arguments passed:
+# - wiki environment instance
+# - tag content
+# - dictionary of attributes
+
+# quote example:
+# quote
+from cgi import escape
+
+def hook_quote(env, body, attributes={}):
+ text = [u'']
+ if 'cite' in attributes:
+ text.append(u"%s wrote: \n" % escape(attributes['cite']))
+ text.append(body.strip())
+ text.append(u'
')
+ return u'\n'.join(text)
+registerTagHook('quote', hook_quote)
+
+def safe_name(name=None, remove_slashes=True):
+ if name is None:
+ return None
+ name = str2url(name)
+ if remove_slashes:
+ name = re.sub(r"[^a-zA-Z0-9\-_\s\.]", "", name)
+ else:
+ name = re.sub(r"[^a-zA-Z0-9\-_\s\.\/]", "", name)
+ name = re.sub(r"[\s\._]", "-", name)
+ name = re.sub(r"[-]+", "-", name)
+ return name.strip("-").lower()
+
+def str2url(str):
+ """
+ Takes a UTF-8 string and replaces all characters with the equivalent in 7-bit
+ ASCII. It returns a plain ASCII string usable in URLs.
+ """
+ try:
+ str = str.encode('utf-8')
+ except:
+ pass
+ mfrom = "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîï"
+ to = "AAAAAAECEEEEIIIIDNOOOOOOUUUUYSaaaaaaaceeeeiiii"
+ mfrom += "ñòóôõöøùúûüýÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģ"
+ to += "noooooouuuuyyaaaaaaccccccccddddeeeeeeeeeegggggggg"
+ mfrom += "ĤĥĦħĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘř"
+ to += "hhhhiiiiiiiiiijjkkkllllllllllnnnnnnnnnoooooooorrrrrr"
+ mfrom += "ŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƂƃƄƅƇƈƉƊƐƑƒƓƔ"
+ to += "ssssssssttttttuuuuuuuuuuuuwwyyyzzzzzzfbbbbbccddeffgv"
+ mfrom += "ƖƗƘƙƚƝƞƟƠƤƦƫƬƭƮƯưƱƲƳƴƵƶǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩ"
+ to += "likklnnoopettttuuuuyyzzaaiioouuuuuuuuuueaaaaeeggggkk"
+ mfrom += "ǪǫǬǭǰǴǵǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȞȟȤȥȦȧȨȩ"
+ to += "oooojggpnnaaeeooaaaaeeeeiiiioooorrrruuuusstthhzzaaee"
+ mfrom += "ȪȫȬȭȮȯȰȱȲȳḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫ"
+ to += "ooooooooyyaabbbbbbccddddddddddeeeeeeeeeeffgghhhhhhhhhh"
+ mfrom += "ḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟ"
+ to += "iiiikkkkkkllllllllmmmmmmnnnnnnnnoooooooopppprrrrrrrr"
+ mfrom += "ṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕ"
+ to += "ssssssssssttttttttuuuuuuuuuuvvvvwwwwwwwwwwxxxxxyzzzzzz"
+ mfrom += "ẖẗẘẙẚẛẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊị"
+ to += "htwyafaaaaaaaaaaaaaaaaaaaaaaaaeeeeeeeeeeeeeeeeiiii"
+ mfrom += "ỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ"
+ to += "oooooooooooooooooooooooouuuuuuuuuuuuuuyyyyyyyy"
+ for i in zip(mfrom, to):
+ str = str.replace(*i)
+ return str