0004-remove-support-for-announces-37967.patch
auquotidien/auquotidien.py | ||
---|---|---|
6 | 6 | |
7 | 7 |
from modules import admin |
8 | 8 |
from modules import backoffice |
9 |
from modules import announces_ui |
|
10 | 9 |
from modules import categories_admin |
11 | 10 |
from modules import payments_ui |
12 | 11 |
from modules import strongbox_ui |
... | ... | |
26 | 25 | |
27 | 26 |
rdb.items = [] |
28 | 27 | |
29 |
rdb.register_directory('announces', announces_ui.AnnouncesDirectory()) |
|
30 |
rdb.register_menu_item('announces/', _('Announces')) |
|
31 | ||
32 | 28 |
rdb.register_directory('payments', payments_ui.PaymentsDirectory()) |
33 | 29 |
rdb.register_menu_item('payments/', _('Payments')) |
34 | 30 |
auquotidien/modules/admin.py | ||
---|---|---|
22 | 22 | |
23 | 23 | |
24 | 24 |
class PanelDirectory(Directory): |
25 |
_q_exports = ['', 'update', 'announces', 'permissions',
|
|
26 |
'announce_themes', 'strongbox', 'domino']
|
|
25 |
_q_exports = ['', 'update', 'permissions', |
|
26 |
'strongbox', 'domino'] |
|
27 | 27 |
label = N_('Control Panel') |
28 | 28 | |
29 | 29 |
domino = AbeliumDominoDirectory() |
... | ... | |
37 | 37 |
if string_value: |
38 | 38 |
form.get_widget('mobile_mask').value = string_value.upper() |
39 | 39 | |
40 |
def announces(self): |
|
41 |
announces_cfg = get_cfg('announces', {}) |
|
42 |
sms_cfg = get_cfg('sms', {}) |
|
43 |
form = Form(enctype='multipart/form-data') |
|
44 |
hint = "" |
|
45 |
if sms_cfg.get('mode','') in ("none",""): |
|
46 |
hint = htmltext(_('You must also <a href="%s">configure your SMS provider</a>') % "../settings/sms") |
|
47 | ||
48 |
form.add(CheckboxWidget, 'sms_support', title = _('SMS support'), |
|
49 |
hint = hint, value = announces_cfg.get('sms_support', 0)) |
|
50 |
form.add(StringWidget, 'mobile_mask', title = _('Mask for mobile numbers'), |
|
51 |
hint = _('example: 06XXXXXXXX'), |
|
52 |
value = announces_cfg.get('mobile_mask','')) |
|
53 |
form.add_submit('submit', _('Submit')) |
|
54 |
form.add_submit('cancel', _('Cancel')) |
|
55 | ||
56 |
self._verify_mask(form) |
|
57 | ||
58 |
if form.get_widget('cancel').parse(): |
|
59 |
return redirect('..') |
|
60 | ||
61 |
if not form.is_submitted() or form.has_errors(): |
|
62 |
get_response().breadcrumb.append(('aq/announces', _('Announces Options'))) |
|
63 |
html_top('settings', _('Announces Options')) |
|
64 |
r = TemplateIO(html=True) |
|
65 |
r += htmltext('<h2>%s</h2>') % _('Announces Options') |
|
66 |
r += form.render() |
|
67 |
return r.getvalue() |
|
68 |
else: |
|
69 |
from wcs.admin.settings import cfg_submit |
|
70 |
cfg_submit(form, 'announces', ('sms_support','mobile_mask')) |
|
71 |
return redirect('..') |
|
72 | ||
73 | 40 |
def permissions(self): |
74 | 41 |
permissions_cfg = get_cfg('aq-permissions', {}) |
75 | 42 |
form = Form(enctype='multipart/form-data') |
76 | 43 |
form.add(SingleSelectWidget, 'forms', title = _('Admin role for forms'), |
77 | 44 |
value = permissions_cfg.get('forms', None), |
78 | 45 |
options = [(None, _('Nobody'), None)] + get_user_roles()) |
79 |
if get_publisher().has_site_option('auquotidien-announces'): |
|
80 |
form.add(SingleSelectWidget, 'announces', title = _('Admin role for announces'), |
|
81 |
value = permissions_cfg.get('announces', None), |
|
82 |
options = [(None, _('Nobody'), None)] + get_user_roles()) |
|
83 | 46 |
if get_publisher().has_site_option('auquotidien-payments'): |
84 | 47 |
form.add(SingleSelectWidget, 'payments', title = _('Admin role for payments'), |
85 | 48 |
value = permissions_cfg.get('payments', None), |
... | ... | |
104 | 67 |
else: |
105 | 68 |
from wcs.admin.settings import cfg_submit |
106 | 69 |
cfg_submit(form, 'aq-permissions', |
107 |
('forms', 'announces', 'payments', 'strongbox')) |
|
108 |
return redirect('..') |
|
109 | ||
110 |
def announce_themes(self): |
|
111 |
misc_cfg = get_cfg('misc', {}) |
|
112 |
form = Form(enctype='multipart/form-data') |
|
113 |
form.add(WidgetList, 'announce_themes', title = _('Announce Themes'), |
|
114 |
value = misc_cfg.get('announce_themes', []), |
|
115 |
elemnt_type = StringWidget, |
|
116 |
add_element_label = _('Add Theme'), |
|
117 |
element_kwargs = {str('render_br'): False, str('size'): 30}) |
|
118 | ||
119 |
form.add_submit('submit', _('Submit')) |
|
120 |
form.add_submit('cancel', _('Cancel')) |
|
121 | ||
122 |
if form.get_widget('cancel').parse(): |
|
123 |
return redirect('..') |
|
124 | ||
125 |
if not form.is_submitted() or form.has_errors(): |
|
126 |
get_response().breadcrumb.append(('aq/announce_themes', _('Announce Themes'))) |
|
127 |
html_top('settings', _('Announce Themes')) |
|
128 |
r = TemplateIO(html=True) |
|
129 |
r += htmltext('<h2>%s</h2>') % _('Announce Themes') |
|
130 |
r += form.render() |
|
131 |
return r.getvalue() |
|
132 |
else: |
|
133 |
from wcs.admin.settings import cfg_submit |
|
134 |
cfg_submit(form, 'misc', ('announce_themes',)) |
|
70 |
('forms', 'payments', 'strongbox')) |
|
135 | 71 |
return redirect('..') |
136 | 72 | |
137 | 73 |
def strongbox(self): |
... | ... | |
163 | 99 | |
164 | 100 |
class SettingsDirectory(wcs.admin.settings.SettingsDirectory): |
165 | 101 |
def _q_index(self): |
166 |
if not (get_publisher().has_site_option('auquotidien-announces') or |
|
167 |
get_publisher().has_site_option('auquotidien-payments') or |
|
102 |
if not (get_publisher().has_site_option('auquotidien-payments') or |
|
168 | 103 |
get_publisher().has_site_option('auquotidien-strongvox')): |
169 | 104 |
return super(SettingsDirectory, self)._q_index() |
170 | 105 |
r = TemplateIO(html=True) |
... | ... | |
173 | 108 |
r += htmltext('<div class="bo-block">') |
174 | 109 |
r += htmltext('<h2>%s</h2>') % _('Extra Options') |
175 | 110 |
r += htmltext('<ul>') |
176 |
if get_publisher().has_site_option('auquotidien-announces'): |
|
177 |
r += htmltext('<li><a href="aq/announces">%s</a></li>') % _('Announces Options') |
|
178 | 111 |
r += htmltext('<li><a href="aq/permissions">%s</a></li>') % _('Permissions') |
179 |
if get_publisher().has_site_option('auquotidien-announces'): |
|
180 |
r += htmltext('<li><a href="aq/announce_themes">%s</a></li>') % _('Announce Themes') |
|
181 | 112 |
if get_publisher().has_site_option('strongbox'): |
182 | 113 |
r += htmltext('<li><a href="aq/strongbox">%s</a></li>') % _('Strongbox Support') |
183 | 114 |
if get_publisher().has_site_option('domino'): |
auquotidien/modules/announces.py | ||
---|---|---|
1 |
import time |
|
2 | ||
3 |
from quixote import get_publisher |
|
4 | ||
5 |
from quixote.html import htmlescape |
|
6 | ||
7 |
from wcs.qommon import _ |
|
8 |
from wcs.qommon.storage import StorableObject |
|
9 |
from wcs.qommon import get_cfg, get_logger |
|
10 |
from wcs.qommon import errors |
|
11 |
from wcs.qommon import misc |
|
12 | ||
13 |
from wcs.qommon import emails |
|
14 |
from wcs.qommon.sms import SMS |
|
15 |
from wcs.qommon.admin.emails import EmailsDirectory |
|
16 | ||
17 |
class AnnounceSubscription(StorableObject): |
|
18 |
_names = 'announce-subscriptions' |
|
19 |
_indexes = ['user_id'] |
|
20 | ||
21 |
user_id = None |
|
22 |
email = None |
|
23 |
sms = None |
|
24 |
enabled = True |
|
25 |
enabled_sms = False |
|
26 |
enabled_themes = None |
|
27 | ||
28 |
def remove(self, type=None): |
|
29 |
""" type (string) : email or sms """ |
|
30 |
if type == "email": |
|
31 |
self.email = None |
|
32 |
elif type == "sms": |
|
33 |
self.sms = None |
|
34 |
self.enabled_sms = False |
|
35 |
if not type or (not self.sms and not self.email): |
|
36 |
self.remove_self() |
|
37 |
else: |
|
38 |
self.store() |
|
39 |
|
|
40 |
def get_user(self): |
|
41 |
if self.user_id: |
|
42 |
try: |
|
43 |
return get_publisher().user_class.get(self.user_id) |
|
44 |
except KeyError: |
|
45 |
return None |
|
46 |
return None |
|
47 |
user = property(get_user) |
|
48 | ||
49 | ||
50 |
class Announce(StorableObject): |
|
51 |
_names = 'announces' |
|
52 | ||
53 |
title = None |
|
54 |
text = None |
|
55 | ||
56 |
hidden = False |
|
57 | ||
58 |
publication_time = None |
|
59 |
modification_time = None |
|
60 |
expiration_time = None |
|
61 |
sent_by_email_time = None |
|
62 |
sent_by_sms_time = None |
|
63 |
theme = None |
|
64 | ||
65 |
position = None |
|
66 | ||
67 |
def sort_by_position(cls, links): |
|
68 |
def cmp_position(x, y): |
|
69 |
if x.position == y.position: |
|
70 |
return 0 |
|
71 |
if x.position is None: |
|
72 |
return 1 |
|
73 |
if y.position is None: |
|
74 |
return -1 |
|
75 |
return cmp(x.position, y.position) |
|
76 |
links.sort(cmp_position) |
|
77 |
sort_by_position = classmethod(sort_by_position) |
|
78 | ||
79 |
def get_atom_entry(self): |
|
80 |
from pyatom import pyatom |
|
81 |
entry = pyatom.Entry() |
|
82 |
entry.id = self.get_url() |
|
83 |
entry.title = self.title |
|
84 | ||
85 |
entry.content.attrs['type'] = 'html' |
|
86 |
entry.content.text = str('<p>' + htmlescape( |
|
87 |
unicode(self.text, get_publisher().site_charset).encode('utf-8')) + '</p>') |
|
88 | ||
89 |
link = pyatom.Link(self.get_url()) |
|
90 |
entry.links.append(link) |
|
91 | ||
92 |
if self.publication_time: |
|
93 |
entry.published = misc.format_time(self.publication_time, |
|
94 |
'%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ', |
|
95 |
gmtime = True) |
|
96 | ||
97 |
if self.modification_time: |
|
98 |
entry.updated = misc.format_time(self.modification_time, |
|
99 |
'%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ', |
|
100 |
gmtime = True) |
|
101 | ||
102 |
return entry |
|
103 | ||
104 |
def get_url(self): |
|
105 |
return '%s/announces/%s/' % (get_publisher().get_frontoffice_url(), self.id) |
|
106 | ||
107 |
def store(self): |
|
108 |
self.modification_time = time.gmtime() |
|
109 |
StorableObject.store(self) |
|
110 | ||
111 |
def email(self, job=None): |
|
112 |
self.sent_by_email_time = time.gmtime() |
|
113 |
StorableObject.store(self) |
|
114 | ||
115 |
data = { |
|
116 |
'title': self.title, |
|
117 |
'text': self.text |
|
118 |
} |
|
119 | ||
120 |
subscribers = AnnounceSubscription.select(lambda x: x.enabled) |
|
121 | ||
122 |
rcpts = [] |
|
123 |
for l in subscribers: |
|
124 |
if self.theme: |
|
125 |
if l.enabled_themes is not None: |
|
126 |
if self.theme not in l.enabled_themes: |
|
127 |
continue |
|
128 |
if l.user and l.user.email: |
|
129 |
rcpts.append(l.user.email) |
|
130 |
elif l.email: |
|
131 |
rcpts.append(l.email) |
|
132 | ||
133 |
emails.custom_template_email('aq-announce', data, email_rcpt=rcpts, hide_recipients=True) |
|
134 | ||
135 |
def sms(self, job=None): |
|
136 |
self.sent_by_sms_time = time.gmtime() |
|
137 |
StorableObject.store(self) |
|
138 | ||
139 |
subscribers = AnnounceSubscription.select(lambda x: x.enabled_sms) |
|
140 | ||
141 |
rcpts = [] |
|
142 |
for sub in subscribers: |
|
143 |
if self.theme: |
|
144 |
if sub.enabled_themes is not None: |
|
145 |
if self.theme not in sub.enabled_themes: |
|
146 |
continue |
|
147 |
if sub.sms: |
|
148 |
rcpts.append(sub.sms) |
|
149 | ||
150 |
sms_cfg = get_cfg('sms', {}) |
|
151 |
sender = sms_cfg.get('sender', 'AuQuotidien')[:11] |
|
152 |
message = "%s: %s" % (self.title, self.text) |
|
153 |
mode = sms_cfg.get('mode', 'none') |
|
154 |
sms = SMS.get_sms_class(mode) |
|
155 |
try: |
|
156 |
sms.send(sender, rcpts, message[:160]) |
|
157 |
except errors.SMSError, e: |
|
158 |
get_logger().error(e) |
|
159 | ||
160 |
def get_published_announces(cls): |
|
161 |
announces = cls.select(lambda x: not x.hidden) |
|
162 |
announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time, |
|
163 |
y.publication_time or y.modification_time)) |
|
164 |
announces = [x for x in announces if x.publication_time < time.gmtime() |
|
165 |
and (x.expiration_time is None or x.expiration_time > time.gmtime())] |
|
166 |
announces.reverse() |
|
167 |
return announces |
|
168 |
get_published_announces = classmethod(get_published_announces) |
|
169 | ||
170 | ||
171 |
EmailsDirectory.register('aq-announce', |
|
172 |
N_('Publication of announce to subscriber'), |
|
173 |
N_('Available variables: title, text'), |
|
174 |
default_subject = N_('Announce: [title]'), |
|
175 |
default_body = N_("""\ |
|
176 |
[text] |
|
177 | ||
178 |
-- |
|
179 |
This is an announce sent to you by your city, you can opt to not receive |
|
180 |
those messages anymore on the city website. |
|
181 |
""")) |
|
182 |
auquotidien/modules/announces_ui.py | ||
---|---|---|
1 |
from quixote import get_request, get_response, get_session, redirect |
|
2 |
from quixote.directory import Directory, AccessControlled |
|
3 |
from quixote.html import htmltext, TemplateIO |
|
4 | ||
5 |
import wcs |
|
6 | ||
7 |
from wcs.qommon import _ |
|
8 |
from wcs.qommon.backoffice.menu import html_top |
|
9 |
from wcs.qommon.admin.menu import command_icon |
|
10 |
from wcs.qommon import get_cfg |
|
11 |
from wcs.qommon import errors |
|
12 |
from wcs.qommon.form import * |
|
13 |
from wcs.qommon.afterjobs import AfterJob |
|
14 | ||
15 |
from .announces import Announce, AnnounceSubscription |
|
16 | ||
17 | ||
18 |
class SubscriptionDirectory(Directory): |
|
19 |
_q_exports = ['delete_email', "delete_sms"] |
|
20 | ||
21 |
def __init__(self, subscription): |
|
22 |
self.subscription = subscription |
|
23 | ||
24 |
def delete_email(self): |
|
25 |
form = Form(enctype='multipart/form-data') |
|
26 |
form.widgets.append(HtmlWidget('<p>%s</p>' % _( |
|
27 |
'You are about to delete this subscription.'))) |
|
28 |
form.add_submit('submit', _('Submit')) |
|
29 |
form.add_submit('cancel', _('Cancel')) |
|
30 |
if form.get_submit() == 'cancel': |
|
31 |
return redirect('..') |
|
32 |
if not form.is_submitted() or form.has_errors(): |
|
33 |
get_response().breadcrumb.append(('delete', _('Delete'))) |
|
34 |
html_top('announces', title = _('Delete Subscription')) |
|
35 |
r = TemplateIO(html=True) |
|
36 |
r += htmltext('<h2>%s</h2>') % _('Deleting Subscription') |
|
37 |
r += form.render() |
|
38 |
return r.getvalue() |
|
39 |
else: |
|
40 |
self.subscription.remove("email") |
|
41 |
return redirect('..') |
|
42 | ||
43 |
def delete_sms(self): |
|
44 |
form = Form(enctype='multipart/form-data') |
|
45 |
form.widgets.append(HtmlWidget('<p>%s</p>' % _( |
|
46 |
'You are about to delete this subscription.'))) |
|
47 |
form.add_submit('submit', _('Submit')) |
|
48 |
form.add_submit('cancel', _('Cancel')) |
|
49 |
if form.get_submit() == 'cancel': |
|
50 |
return redirect('..') |
|
51 |
if not form.is_submitted() or form.has_errors(): |
|
52 |
get_response().breadcrumb.append(('delete', _('Delete'))) |
|
53 |
html_top('announces', title = _('Delete Subscription')) |
|
54 |
r = TemplateIO(html=True) |
|
55 |
r += htmltext('<h2>%s</h2>') % _('Deleting Subscription') |
|
56 |
r += form.render() |
|
57 |
return r.getvalue() |
|
58 |
else: |
|
59 |
self.subscription.remove("sms") |
|
60 |
return redirect('..') |
|
61 | ||
62 | ||
63 |
class SubscriptionsDirectory(Directory): |
|
64 |
_q_exports = [''] |
|
65 | ||
66 |
def _q_traverse(self, path): |
|
67 |
get_response().breadcrumb.append(('subscriptions', _('Subscriptions'))) |
|
68 |
return Directory._q_traverse(self, path) |
|
69 | ||
70 |
def _q_index(self): |
|
71 |
html_top('announces', _('Announces Subscribers')) |
|
72 |
r = TemplateIO(html=True) |
|
73 | ||
74 |
r += htmltext('<h2>%s</h2>') % _('Announces Subscribers') |
|
75 | ||
76 |
subscribers = AnnounceSubscription.select() |
|
77 |
r += htmltext('<ul class="biglist" id="subscribers-list">') |
|
78 |
for l in subscribers: |
|
79 |
if l.email: |
|
80 |
if l.enabled is False: |
|
81 |
r += htmltext('<li class="disabled">') |
|
82 |
else: |
|
83 |
r += htmltext('<li>') |
|
84 |
r += htmltext('<strong class="label">') |
|
85 |
if l.user: |
|
86 |
r += l.user.display_name |
|
87 |
elif l.email: |
|
88 |
r += l.email |
|
89 |
r += htmltext('</strong>') |
|
90 |
r += htmltext('<p class="details">') |
|
91 |
if l.user: |
|
92 |
r += l.user.email |
|
93 |
r += htmltext('</p>') |
|
94 |
r += htmltext('<p class="commands">') |
|
95 |
r += command_icon('%s/delete_email' % l.id, 'remove', popup = True) |
|
96 |
r += htmltext('</p></li>') |
|
97 |
r += htmltext('</li>') |
|
98 |
if l.sms: |
|
99 |
if l.enabled_sms is False: |
|
100 |
r += htmltext('<li class="disabled">') |
|
101 |
else: |
|
102 |
r += htmltext('<li>') |
|
103 |
r += htmltext('<strong class="label">') |
|
104 |
if l.user: |
|
105 |
r += l.user.display_name |
|
106 |
elif l.email: |
|
107 |
r += l.email |
|
108 |
r += htmltext('</strong>') |
|
109 |
r += htmltext('<p class="details">') |
|
110 |
r += l.sms |
|
111 |
r += htmltext('</p>') |
|
112 |
r += htmltext('<p class="commands">') |
|
113 |
r += command_icon('%s/delete_sms' % l.id, 'remove', popup = True) |
|
114 |
r += htmltext('</p></li>') |
|
115 |
r += htmltext('</li>') |
|
116 |
r += htmltext('</ul>') |
|
117 |
return r.getvalue() |
|
118 | ||
119 |
def _q_lookup(self, component): |
|
120 |
try: |
|
121 |
sub = AnnounceSubscription.get(component) |
|
122 |
except KeyError: |
|
123 |
raise errors.TraversalError() |
|
124 |
get_response().breadcrumb.append((str(sub.id), str(sub.id))) |
|
125 |
return SubscriptionDirectory(sub) |
|
126 | ||
127 |
def listing(self): |
|
128 |
return redirect('.') |
|
129 | ||
130 |
class AnnounceDirectory(Directory): |
|
131 |
_q_exports = ['', 'edit', 'delete', 'email', 'sms'] |
|
132 | ||
133 |
def __init__(self, announce): |
|
134 |
self.announce = announce |
|
135 | ||
136 |
def _q_index(self): |
|
137 |
form = Form(enctype='multipart/form-data') |
|
138 |
get_response().filter['sidebar'] = self.get_sidebar() |
|
139 | ||
140 |
if self.announce.sent_by_email_time is None: |
|
141 |
form.add_submit('email', _('Send email')) |
|
142 | ||
143 |
announces_cfg = get_cfg('announces', {}) |
|
144 |
if announces_cfg.get('sms_support', 0) and self.announce.sent_by_sms_time is None: |
|
145 |
form.add_submit('sms', _('Send SMS')) |
|
146 | ||
147 |
if form.get_submit() == 'edit': |
|
148 |
return redirect('edit') |
|
149 |
if form.get_submit() == 'delete': |
|
150 |
return redirect('delete') |
|
151 |
if form.get_submit() == 'email': |
|
152 |
return redirect('email') |
|
153 |
if form.get_submit() == 'sms': |
|
154 |
return redirect('sms') |
|
155 | ||
156 |
html_top('announces', title = _('Announce: %s') % self.announce.title) |
|
157 |
r = TemplateIO(html=True) |
|
158 |
r += htmltext('<h2>%s</h2>') % _('Announce: %s') % self.announce.title |
|
159 |
r += htmltext('<div class="bo-block">') |
|
160 |
r += htmltext('<p>') |
|
161 |
r += self.announce.text |
|
162 |
r += htmltext('</p>') |
|
163 |
r += htmltext('</div>') |
|
164 | ||
165 |
if form.get_submit_widgets(): |
|
166 |
r += form.render() |
|
167 | ||
168 |
return r.getvalue() |
|
169 | ||
170 |
def get_sidebar(self): |
|
171 |
r = TemplateIO(html=True) |
|
172 |
r += htmltext('<ul>') |
|
173 |
r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit') |
|
174 |
r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete') |
|
175 |
r += htmltext('</ul>') |
|
176 |
return r.getvalue() |
|
177 | ||
178 |
def email(self): |
|
179 |
if get_request().form.get('job'): |
|
180 |
try: |
|
181 |
job = AfterJob.get(get_request().form.get('job')) |
|
182 |
except KeyError: |
|
183 |
return redirect('..') |
|
184 |
html_top('announces', title = _('Announce: %s') % self.announce.title) |
|
185 |
r = TemplateIO(html=True) |
|
186 |
get_response().add_javascript(['jquery.js', 'afterjob.js']) |
|
187 |
r += htmltext('<dl class="job-status">') |
|
188 |
r += htmltext('<dt>') |
|
189 |
r += _(job.label) |
|
190 |
r += htmltext('</dt>') |
|
191 |
r += htmltext('<dd>') |
|
192 |
r += htmltext('<span class="afterjob" id="%s">') % job.id |
|
193 |
r += _(job.status) |
|
194 |
r += htmltext('</span>') |
|
195 |
r += htmltext('</dd>') |
|
196 |
r += htmltext('</dl>') |
|
197 | ||
198 |
r += htmltext('<div class="done">') |
|
199 |
r += htmltext('<a href="../">%s</a>') % _('Back') |
|
200 |
r += htmltext('</div>') |
|
201 | ||
202 |
return r.getvalue() |
|
203 |
else: |
|
204 |
job = get_response().add_after_job( |
|
205 |
str(N_('Sending emails for announce')), |
|
206 |
self.announce.email) |
|
207 |
return redirect('email?job=%s' % job.id) |
|
208 | ||
209 |
def sms(self): |
|
210 |
if get_request().form.get('job'): |
|
211 |
try: |
|
212 |
job = AfterJob.get(get_request().form.get('job')) |
|
213 |
except KeyError: |
|
214 |
return redirect('..') |
|
215 |
html_top('announces', title = _('Announce: %s') % self.announce.title) |
|
216 |
get_response().add_javascript(['jquery.js', 'afterjob.js']) |
|
217 |
r = TemplateIO(html=True) |
|
218 |
r += htmltext('<dl class="job-status">') |
|
219 |
r += htmltext('<dt>') |
|
220 |
r += _(job.label) |
|
221 |
r += htmltext('</dt>') |
|
222 |
r += htmltext('<dd>') |
|
223 |
r += htmltext('<span class="afterjob" id="%s">') % job.id |
|
224 |
r += _(job.status) |
|
225 |
r += htmltext('</span>') |
|
226 |
r += htmltext('</dd>') |
|
227 |
r += htmltext('</dl>') |
|
228 | ||
229 |
r += htmltext('<div class="done">') |
|
230 |
r += htmltext('<a href="../">%s</a>') % _('Back') |
|
231 |
r += htmltext('</div>') |
|
232 | ||
233 |
return r.getvalue() |
|
234 |
else: |
|
235 |
job = get_response().add_after_job( |
|
236 |
str(N_('Sending sms for announce')), |
|
237 |
self.announce.sms) |
|
238 |
return redirect('sms?job=%s' % job.id) |
|
239 | ||
240 |
def edit(self): |
|
241 |
form = self.form() |
|
242 |
if form.get_submit() == 'cancel': |
|
243 |
return redirect('.') |
|
244 | ||
245 |
if form.is_submitted() and not form.has_errors(): |
|
246 |
self.submit(form) |
|
247 |
return redirect('..') |
|
248 | ||
249 |
html_top('announces', title = _('Edit Announce: %s') % self.announce.title) |
|
250 |
r = TemplateIO(html=True) |
|
251 |
r += htmltext('<h2>%s</h2>') % _('Edit Announce: %s') % self.announce.title |
|
252 |
r += form.render() |
|
253 |
return r.getvalue() |
|
254 | ||
255 |
def form(self): |
|
256 |
form = Form(enctype='multipart/form-data') |
|
257 |
form.add(StringWidget, 'title', title = _('Title'), required = True, |
|
258 |
value = self.announce.title) |
|
259 |
if self.announce.publication_time: |
|
260 |
pub_time = time.strftime(misc.date_format(), self.announce.publication_time) |
|
261 |
else: |
|
262 |
pub_time = None |
|
263 |
form.add(DateWidget, 'publication_time', title = _('Publication Time'), |
|
264 |
value = pub_time) |
|
265 |
if self.announce.expiration_time: |
|
266 |
exp_time = time.strftime(misc.date_format(), self.announce.expiration_time) |
|
267 |
else: |
|
268 |
exp_time = None |
|
269 |
form.add(DateWidget, 'expiration_time', title = _('Expiration Time'), |
|
270 |
value = exp_time) |
|
271 |
form.add(TextWidget, 'text', title = _('Text'), required = True, |
|
272 |
value = self.announce.text, rows = 10, cols = 70) |
|
273 |
if get_cfg('misc', {}).get('announce_themes'): |
|
274 |
form.add(SingleSelectWidget, 'theme', title = _('Announce Theme'), |
|
275 |
value = self.announce.theme, |
|
276 |
options = get_cfg('misc', {}).get('announce_themes')) |
|
277 |
form.add(CheckboxWidget, 'hidden', title = _('Hidden'), |
|
278 |
value = self.announce.hidden) |
|
279 |
form.add_submit('submit', _('Submit')) |
|
280 |
form.add_submit('cancel', _('Cancel')) |
|
281 |
return form |
|
282 | ||
283 |
def submit(self, form): |
|
284 |
for k in ('title', 'text', 'hidden', 'theme'): |
|
285 |
widget = form.get_widget(k) |
|
286 |
if widget: |
|
287 |
setattr(self.announce, k, widget.parse()) |
|
288 |
for k in ('publication_time', 'expiration_time'): |
|
289 |
widget = form.get_widget(k) |
|
290 |
if widget: |
|
291 |
wid_time = widget.parse() |
|
292 |
if wid_time: |
|
293 |
setattr(self.announce, k, time.strptime(wid_time, misc.date_format())) |
|
294 |
else: |
|
295 |
setattr(self.announce, k, None) |
|
296 |
self.announce.store() |
|
297 | ||
298 |
def delete(self): |
|
299 |
form = Form(enctype='multipart/form-data') |
|
300 |
form.widgets.append(HtmlWidget('<p>%s</p>' % _( |
|
301 |
'You are about to irrevocably delete this announce.'))) |
|
302 |
form.add_submit('submit', _('Submit')) |
|
303 |
form.add_submit('cancel', _('Cancel')) |
|
304 |
if form.get_submit() == 'cancel': |
|
305 |
return redirect('..') |
|
306 |
if not form.is_submitted() or form.has_errors(): |
|
307 |
get_response().breadcrumb.append(('delete', _('Delete'))) |
|
308 |
html_top('announces', title = _('Delete Announce')) |
|
309 |
r = TemplateIO(html=True) |
|
310 |
r += htmltext('<h2>%s</h2>') % _('Deleting Announce: %s') % self.announce.title |
|
311 |
r += form.render() |
|
312 |
return r.getvalue() |
|
313 |
else: |
|
314 |
self.announce.remove_self() |
|
315 |
return redirect('..') |
|
316 | ||
317 | ||
318 |
class AnnouncesDirectory(AccessControlled, Directory): |
|
319 |
_q_exports = ['', 'new', 'listing', 'subscriptions', 'update_order', 'log'] |
|
320 |
label = N_('Announces') |
|
321 | ||
322 |
subscriptions = SubscriptionsDirectory() |
|
323 | ||
324 |
def is_accessible(self, user): |
|
325 |
from .backoffice import check_visibility |
|
326 |
return check_visibility('announces', user) |
|
327 | ||
328 |
def _q_access(self): |
|
329 |
user = get_request().user |
|
330 |
if not user: |
|
331 |
raise errors.AccessUnauthorizedError() |
|
332 | ||
333 |
if not self.is_accessible(user): |
|
334 |
raise errors.AccessForbiddenError( |
|
335 |
public_msg = _('You are not allowed to access Announces Management'), |
|
336 |
location_hint = 'backoffice') |
|
337 | ||
338 |
get_response().breadcrumb.append(('announces/', _('Announces'))) |
|
339 | ||
340 |
def _q_index(self): |
|
341 |
html_top('announces', _('Announces')) |
|
342 |
r = TemplateIO(html=True) |
|
343 | ||
344 |
get_response().filter['sidebar'] = self.get_sidebar() |
|
345 | ||
346 |
announces = Announce.select() |
|
347 |
announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time, |
|
348 |
y.publication_time or y.modification_time)) |
|
349 |
announces.reverse() |
|
350 | ||
351 |
r += htmltext('<ul class="biglist" id="announces-list">') |
|
352 |
for l in announces: |
|
353 |
announce_id = l.id |
|
354 |
if l.hidden: |
|
355 |
r += htmltext('<li class="disabled" class="biglistitem" id="itemId_%s">') % announce_id |
|
356 |
else: |
|
357 |
r += htmltext('<li class="biglistitem" id="itemId_%s">') % announce_id |
|
358 |
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (l.id, l.title) |
|
359 |
if l.publication_time: |
|
360 |
r += htmltext('<p class="details">') |
|
361 |
r += time.strftime(misc.date_format(), l.publication_time) |
|
362 |
r += htmltext('</p>') |
|
363 |
r += htmltext('</li>') |
|
364 |
r += htmltext('</ul>') |
|
365 |
return r.getvalue() |
|
366 | ||
367 |
def get_sidebar(self): |
|
368 |
r = TemplateIO(html=True) |
|
369 |
r += htmltext('<ul id="sidebar-actions">') |
|
370 |
r += htmltext(' <li><a class="new-item" href="new">%s</a></li>') % _('New') |
|
371 |
r += htmltext(' <li><a href="subscriptions/">%s</a></li>') % _('Subscriptions') |
|
372 |
r += htmltext(' <li><a href="log">%s</a></li>') % _('Log') |
|
373 |
r += htmltext('</ul>') |
|
374 |
return r.getvalue() |
|
375 | ||
376 |
def log(self): |
|
377 |
announces = Announce.select() |
|
378 |
log = [] |
|
379 |
for l in announces: |
|
380 |
if l.publication_time: |
|
381 |
log.append((l.publication_time, _('Publication'), l)) |
|
382 |
if l.sent_by_email_time: |
|
383 |
log.append((l.sent_by_email_time, _('Email'), l)) |
|
384 |
if l.sent_by_sms_time: |
|
385 |
log.append((l.sent_by_sms_time, _('SMS'), l)) |
|
386 |
log.sort() |
|
387 | ||
388 |
get_response().breadcrumb.append(('log', _('Log'))) |
|
389 |
html_top('announces', title = _('Log')) |
|
390 |
r = TemplateIO(html=True) |
|
391 | ||
392 |
r += htmltext('<table>') |
|
393 |
r += htmltext('<thead>') |
|
394 |
r += htmltext('<tr>') |
|
395 |
r += htmltext('<th>%s</th>') % _('Time') |
|
396 |
r += htmltext('<th>%s</th>') % _('Type') |
|
397 |
r += htmltext('<td></td>') |
|
398 |
r += htmltext('</tr>') |
|
399 |
r += htmltext('</thead>') |
|
400 |
r += htmltext('<tbody>') |
|
401 |
for log_time, log_type, log_announce in log: |
|
402 |
r += htmltext('<tr>') |
|
403 |
r += htmltext('<td>') |
|
404 |
r += misc.localstrftime(log_time) |
|
405 |
r += htmltext('</td>') |
|
406 |
r += htmltext('<td>') |
|
407 |
r += log_type |
|
408 |
r += htmltext('</td>') |
|
409 |
r += htmltext('<td>') |
|
410 |
r += htmltext('<a href="%s">%s</a>') % (log_announce.id, log_announce.title) |
|
411 |
r += htmltext('</td>') |
|
412 |
r += htmltext('</tr>') |
|
413 |
r += htmltext('</tbody>') |
|
414 |
r += htmltext('</table>') |
|
415 |
return r.getvalue() |
|
416 | ||
417 |
def update_order(self): |
|
418 |
request = get_request() |
|
419 |
new_order = request.form['order'].strip(';').split(';') |
|
420 |
announces = Announce.select() |
|
421 |
dict = {} |
|
422 |
for l in announces: |
|
423 |
dict[str(l.id)] = l |
|
424 |
for i, o in enumerate(new_order): |
|
425 |
dict[o].position = i + 1 |
|
426 |
dict[o].store() |
|
427 |
return 'ok' |
|
428 | ||
429 |
def new(self): |
|
430 |
announce = Announce() |
|
431 |
announce.publication_time = time.gmtime() |
|
432 |
announce_ui = AnnounceDirectory(announce) |
|
433 | ||
434 |
form = announce_ui.form() |
|
435 |
if form.get_submit() == 'cancel': |
|
436 |
return redirect('.') |
|
437 | ||
438 |
if form.is_submitted() and not form.has_errors(): |
|
439 |
announce_ui.submit(form) |
|
440 |
return redirect('%s/' % announce_ui.announce.id) |
|
441 | ||
442 |
get_response().breadcrumb.append(('new', _('New Announce'))) |
|
443 |
html_top('announces', title = _('New Announce')) |
|
444 |
r = TemplateIO(html=True) |
|
445 |
r += htmltext('<h2>%s</h2>') % _('New Announce') |
|
446 |
r += form.render() |
|
447 |
return r.getvalue() |
|
448 | ||
449 |
def _q_lookup(self, component): |
|
450 |
try: |
|
451 |
announce = Announce.get(component) |
|
452 |
except KeyError: |
|
453 |
raise errors.TraversalError() |
|
454 |
get_response().breadcrumb.append((str(announce.id), announce.title)) |
|
455 |
return AnnounceDirectory(announce) |
|
456 | ||
457 |
def listing(self): |
|
458 |
return redirect('.') |
|
459 |
auquotidien/modules/myspace.py | ||
---|---|---|
25 | 25 |
from wcs.formdef import FormDef |
26 | 26 |
import wcs.myspace |
27 | 27 | |
28 |
from .announces import AnnounceSubscription |
|
29 | 28 |
from .strongbox import StrongboxItem, StrongboxType |
30 | 29 |
from .payments import Invoice, Regie, is_payment_supported |
31 | 30 | |
... | ... | |
411 | 410 | |
412 | 411 |
class MyspaceDirectory(wcs.myspace.MyspaceDirectory): |
413 | 412 |
_q_exports = ['', 'profile', 'new', 'password', 'remove', 'drafts', 'forms', |
414 |
'announces', 'strongbox', 'invoices', 'json']
|
|
413 |
'strongbox', 'invoices', 'json'] |
|
415 | 414 | |
416 | 415 |
strongbox = StrongboxDirectory() |
417 | 416 |
invoices = MyInvoicesDirectory() |
... | ... | |
492 | 491 |
r += htmltext(' <strong><a href="remove" rel="popup">%s</a></strong>.') % _('Delete My Account') |
493 | 492 |
r += htmltext('</p>') |
494 | 493 | |
495 |
options = get_cfg('misc', {}).get('announce_themes') |
|
496 |
if options: |
|
497 |
try: |
|
498 |
subscription = AnnounceSubscription.get_on_index( |
|
499 |
get_request().user.id, str('user_id')) |
|
500 |
except KeyError: |
|
501 |
pass |
|
502 |
else: |
|
503 |
r += htmltext('<p class="command"><a href="announces">%s</a></p>') % _( |
|
504 |
'Edit my Subscription to Announces') |
|
505 | ||
506 | 494 |
if user_forms: |
507 | 495 |
r += htmltext('<h3 id="my-forms">%s</h3>') % _('My Forms') |
508 | 496 |
from . import root |
... | ... | |
675 | 663 |
template.html_top(_('Removing Account')) |
676 | 664 |
return form.render() |
677 | 665 | |
678 |
def announces(self): |
|
679 |
options = get_cfg('misc', {}).get('announce_themes') |
|
680 |
if not options: |
|
681 |
raise errors.TraversalError() |
|
682 |
user = get_request().user |
|
683 |
if not user or user.anonymous: |
|
684 |
raise errors.AccessUnauthorizedError() |
|
685 |
subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id')) |
|
686 |
if not subscription: |
|
687 |
raise errors.TraversalError() |
|
688 | ||
689 |
if subscription.enabled_themes is None: |
|
690 |
enabled_themes = options |
|
691 |
else: |
|
692 |
enabled_themes = subscription.enabled_themes |
|
693 | ||
694 |
form = Form(enctype = 'multipart/form-data') |
|
695 |
form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'), |
|
696 |
value=enabled_themes, options=[(x, x, x) for x in options], |
|
697 |
inline=False, required=False) |
|
698 | ||
699 |
form.add_submit('submit', _('Apply Changes')) |
|
700 |
form.add_submit('cancel', _('Cancel')) |
|
701 | ||
702 |
if form.get_submit() == 'cancel': |
|
703 |
return redirect('.') |
|
704 | ||
705 |
if form.is_submitted() and not form.has_errors(): |
|
706 |
chosen_themes = form.get_widget('themes').parse() |
|
707 |
if chosen_themes == options: |
|
708 |
chosen_themes = None |
|
709 |
subscription.enabled_themes = chosen_themes |
|
710 |
subscription.store() |
|
711 |
return redirect('.') |
|
712 | ||
713 |
template.html_top() |
|
714 |
get_response().breadcrumb.append(('announces', _('Announce Subscription'))) |
|
715 |
return form.render() |
|
716 | ||
717 | 666 | |
718 | 667 |
TextsDirectory.register('aq-myspace-invoice', |
719 | 668 |
N_('Message on top of invoices page'), |
auquotidien/modules/pyatom/afl-2.1.txt | ||
---|---|---|
1 |
# Larry Rosen has ceased to use or recommend any version |
|
2 |
# of the Academic Free License below version 2.1 |
|
3 | ||
4 |
The Academic Free License |
|
5 |
v. 2.1 |
|
6 | ||
7 |
This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following notice immediately following the copyright notice for the Original Work: |
|
8 | ||
9 |
Licensed under the Academic Free License version 2.1 |
|
10 | ||
11 |
1) Grant of Copyright License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license to do the following: |
|
12 | ||
13 |
a) to reproduce the Original Work in copies; |
|
14 | ||
15 |
b) to prepare derivative works ("Derivative Works") based upon the Original Work; |
|
16 | ||
17 |
c) to distribute copies of the Original Work and Derivative Works to the public; |
|
18 | ||
19 |
d) to perform the Original Work publicly; and |
|
20 | ||
21 |
e) to display the Original Work publicly. |
|
22 | ||
23 |
2) Grant of Patent License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, to make, use, sell and offer for sale the Original Work and Derivative Works. |
|
24 | ||
25 |
3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor hereby agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work, and by publishing the address of that information repository in a notice immediately following the copyright notice that applies to the Original Work. |
|
26 | ||
27 |
4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior written permission of the Licensor. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor except as expressly stated herein. No patent license is granted to make, use, sell or offer to sell embodiments of any patent claims other than the licensed claims defined in Section 2. No right is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any Original Work that Licensor otherwise would have a right to license. |
|
28 | ||
29 |
5) This section intentionally omitted. |
|
30 | ||
31 |
6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. |
|
32 | ||
33 |
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately proceeding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to Original Work is granted hereunder except under this disclaimer. |
|
34 | ||
35 |
8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to any person for any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to liability for death or personal injury resulting from Licensor's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. |
|
36 | ||
37 |
9) Acceptance and Termination. If You distribute copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. Nothing else but this License (or another written agreement between Licensor and You) grants You permission to create Derivative Works based upon the Original Work or to exercise any of the rights granted in Section 1 herein, and any attempt to do so except under the terms of this License (or another written agreement between Licensor and You) is expressly prohibited by U.S. copyright law, the equivalent laws of other countries, and by international treaty. Therefore, by exercising any of the rights granted to You in Section 1 herein, You indicate Your acceptance of this License and all of its terms and conditions. |
|
38 | ||
39 |
10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. |
|
40 | ||
41 |
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et seq., the equivalent laws of other countries, and international treaty. This section shall survive the termination of this License. |
|
42 | ||
43 |
12) Attorneys Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. |
|
44 | ||
45 |
13) Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. |
|
46 | ||
47 |
14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. |
|
48 | ||
49 |
15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. |
|
50 | ||
51 |
This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. Permission is hereby granted to copy and distribute this license without modification. This license may not be modified without the express written permission of its copyright owner. |
|
52 | ||
53 |
|
|
54 |
auquotidien/modules/pyatom/pyatom.py | ||
---|---|---|
1 |
# pyatom.py -- PyAtom library module |
|
2 | ||
3 |
""" |
|
4 |
PyAtom |
|
5 | ||
6 |
Module to make it really easy to create Atom syndication feeds. |
|
7 | ||
8 |
This module is Copyright (C) 2006 Steve R. Hastings. |
|
9 |
Licensed under the Academic Free License version 2.1 |
|
10 | ||
11 |
You might want to start with the test cases at the end; see how they |
|
12 |
work, and then go back and look at the code in the module. |
|
13 | ||
14 |
I hope you find this useful! |
|
15 | ||
16 |
Steve R. Hastings |
|
17 | ||
18 |
Please send your questions or comments to this email address: |
|
19 | ||
20 |
pyatom@langri.com |
|
21 |
""" |
|
22 | ||
23 | ||
24 | ||
25 |
import re |
|
26 |
import sys |
|
27 |
import time |
|
28 | ||
29 |
s_pyatom_name = "PyAtom" |
|
30 |
s_pyatom_ver = "0.3.9" |
|
31 |
s_pyatom_name_ver = "%s version %s" % (s_pyatom_name, s_pyatom_ver) |
|
32 | ||
33 |
# string constants |
|
34 |
# These string values are used in more than one place. |
|
35 | ||
36 |
s_version = "version" |
|
37 |
s_encoding = "encoding" |
|
38 |
s_standalone = "standalone" |
|
39 | ||
40 |
s_href = "href" |
|
41 |
s_lang = "xml:lang" |
|
42 |
s_link = "link" |
|
43 |
s_term = "term" |
|
44 |
s_type = "type" |
|
45 | ||
46 | ||
47 | ||
48 |
def set_s_indent(s): |
|
49 |
""" |
|
50 |
Set up the globals PyAtom uses to indent its output: |
|
51 |
s_indent, and s_indent_big |
|
52 | ||
53 |
s_indent is the string to indent one level; default is \\t. |
|
54 | ||
55 |
s_indent_big is s_indent concatenated many times. PyAtom uses slice |
|
56 |
copies to get indent strings from s_indent_big. |
|
57 |
""" |
|
58 |
global s_indent |
|
59 |
global s_indent_big |
|
60 |
s_indent = s |
|
61 |
s_indent_big = s*256 |
|
62 | ||
63 |
set_s_indent("\t") |
|
64 | ||
65 | ||
66 | ||
67 |
class TFC(object): |
|
68 |
""" |
|
69 |
class TFC: Tag Format Control. |
|
70 |
Controls how tags are converted to strings. |
|
71 | ||
72 |
Arguments to __init__(): |
|
73 |
level Specifies what indent level to start at for output. Default 0. |
|
74 |
mode Specifies how to format the output: |
|
75 |
mode_terse -- minimal output (no indenting) |
|
76 |
mode_normal -- default |
|
77 |
mode_verbose -- normal, plus some XML comments |
|
78 | ||
79 |
Normally, if an XML item has no data, nothing is printed, but with |
|
80 |
mode_verbose you may get a comment like "Collection with 0 entries". |
|
81 | ||
82 |
Methods: |
|
83 |
b_print_all() |
|
84 |
Return True if TFC set for full printing. |
|
85 |
b_print_terse() |
|
86 |
Return True if TFC set for terse printing. |
|
87 |
b_print_verbose() |
|
88 |
Return True if TFC set for verbose printing. |
|
89 | ||
90 |
indent_by(incr) |
|
91 |
Return a TFC instance that indents by incr columns. |
|
92 |
s_indent(extra_indent=0) |
|
93 |
Return an indent string. |
|
94 |
""" |
|
95 |
mode_terse, mode_normal, mode_verbose = range(3) |
|
96 | ||
97 |
def __init__(self, level=0, mode=mode_normal): |
|
98 |
""" |
|
99 |
Arguments: |
|
100 |
level Specifies what indent level to start at for output. Default 0. |
|
101 |
mode Specifies how to format the output: |
|
102 |
mode_terse -- minimal output (no indenting) |
|
103 |
mode_normal -- default |
|
104 |
mode_verbose -- normal, plus some XML comments |
|
105 | ||
106 |
Normally, if an XML item has no data, nothing is printed, but with |
|
107 |
mode_verbose you may get a comment like "Collection with 0 entries". |
|
108 |
""" |
|
109 |
self.level = level |
|
110 |
self.mode = mode |
|
111 | ||
112 |
def b_print_all(self): |
|
113 |
""" |
|
114 |
Return True if TFC set for full printing. |
|
115 | ||
116 |
Some optional things are usually suppressed, but will be printed |
|
117 |
if the current level is 0. And everything gets printed when |
|
118 |
mode_verbose is set. |
|
119 |
""" |
|
120 |
return self.level == 0 or self.mode == TFC.mode_verbose |
|
121 | ||
122 |
def b_print_terse(self): |
|
123 |
""" |
|
124 |
Return True if TFC set for terse printing. |
|
125 |
""" |
|
126 |
return self.mode == TFC.mode_terse |
|
127 | ||
128 |
def b_print_verbose(self): |
|
129 |
""" |
|
130 |
Return True if TFC set for verbose printing. |
|
131 |
""" |
|
132 |
return self.mode == TFC.mode_verbose |
|
133 | ||
134 |
def indent_by(self, incr): |
|
135 |
""" |
|
136 |
Return a TFC instance that indents by incr columns. |
|
137 | ||
138 |
Pass this to a function that takes a TFC to get a temporary indent. |
|
139 |
""" |
|
140 |
return TFC(self.level + incr, self.mode) |
|
141 |
def s_indent(self, extra_indent=0): |
|
142 |
""" |
|
143 |
Return an indent string. |
|
144 | ||
145 |
Return a string of white space that indents correctly for the |
|
146 |
current TFC settings. If specified, extra_indent will be added |
|
147 |
to the current indent level. |
|
148 |
""" |
|
149 |
if self.mode == TFC.mode_terse: |
|
150 |
return "" |
|
151 |
level = self.level + extra_indent |
|
152 |
return s_indent_big[0:level] |
|
153 | ||
154 | ||
155 | ||
156 |
pat_nbsp = re.compile(r' ') |
|
157 |
def s_entities_to_ws(s): |
|
158 |
""" |
|
159 |
Return a copy of s with HTML whitespace entities replaced by a space. |
|
160 | ||
161 |
Currently just gets rid of HTML non-breaking spaces (" "). |
|
162 |
""" |
|
163 |
if not s: |
|
164 |
return s |
|
165 | ||
166 |
s = re.sub(pat_nbsp, " ", s) |
|
167 |
return s |
|
168 | ||
169 |
def s_normalize_ws(s): |
|
170 |
""" |
|
171 |
Return a copy of string s with each run of whitespace replaced by one space. |
|
172 |
>>> s = "and now\n\n\nfor \t something\v completely\r\n different" |
|
173 |
>>> print s_normalize_ws(s) |
|
174 |
and now for something completely different |
|
175 |
>>> |
|
176 |
""" |
|
177 |
lst = s.split() |
|
178 |
s = " ".join(lst) |
|
179 |
return s |
|
180 |
|
|
181 | ||
182 |
def s_escape_html(s): |
|
183 |
""" |
|
184 |
Return a copy of string s with HTML codes escaped. |
|
185 | ||
186 |
This is useful when you want HTML tags printed literally, rather than |
|
187 |
interpreted. |
|
188 | ||
189 |
>>> print s_escape_html("<head>") |
|
190 |
<head> |
|
191 |
>>> print s_escape_html(" ") |
|
192 |
&nbsp; |
|
193 |
""" |
|
194 |
s = s.replace("&", "&") |
|
195 |
s = s.replace("<", "<") |
|
196 |
s = s.replace(">", ">") |
|
197 |
return s |
|
198 | ||
199 |
def s_create_atom_id(t, domain_name, uri=""): |
|
200 |
""" |
|
201 |
Create ID using Mark Pilgrim's algorithm. |
|
202 | ||
203 |
Algorithm taken from here: |
|
204 |
http://diveintomark.org/archives/2004/05/28/howto-atom-id |
|
205 |
""" |
|
206 | ||
207 |
# ymd (year-month-day) example: 2003-12-13 |
|
208 |
ymd = time.strftime("%Y-%m-%d", t) |
|
209 | ||
210 |
if uri == "": |
|
211 |
# mush (all mushed together) example: 20031213083000 |
|
212 |
mush = time.strftime("%Y%m%d%H%M%S", t) |
|
213 |
uri = "/weblog/" + mush |
|
214 | ||
215 |
# s = "tag:" + domain_name + "," + ymd + ":" + uri |
|
216 |
s = "tag:%s,%s:%s" % (domain_name, ymd, uri) |
|
217 | ||
218 |
s = s.replace("#", "/") |
|
219 | ||
220 |
return s |
|
221 | ||
222 |
s_copyright_multiyear = "Copyright %s %d-%d by %s." |
|
223 |
s_copyright_oneyear = "Copyright %s %d by %s." |
|
224 |
def s_copyright(s_owner, s_csym="(C)", end_year=None, start_year=None): |
|
225 |
""" |
|
226 |
Return a string with a copyright notice. |
|
227 | ||
228 |
s_owner |
|
229 |
string with copyright owner's name. |
|
230 |
s_csym |
|
231 |
string with copyright symbol. (An HTML entity might be good here.) |
|
232 |
end_year |
|
233 |
last year of the copyright. Default is the current year. |
|
234 |
start_year |
|
235 |
first year of the copyright. |
|
236 | ||
237 |
If only end_year is specified, only print one year; if both end_year and |
|
238 |
start_year are specified, print a range. |
|
239 | ||
240 |
To localize the entire copyright message into another language, change |
|
241 |
the global variables with the copyright template: |
|
242 |
s_copyright_multiyear: for a year range |
|
243 |
s_copyright_oneyear: for a single year |
|
244 |
""" |
|
245 |
if not end_year: |
|
246 |
end_year = time.localtime().tm_year |
|
247 | ||
248 |
if start_year: |
|
249 |
return s_copyright_multiyear % (s_csym, start_year, end_year, s_owner) |
|
250 | ||
251 |
return s_copyright_oneyear % (s_csym, end_year, s_owner) |
|
252 | ||
253 | ||
254 | ||
255 |
# Here are all of the possible XML items. |
|
256 |
# |
|
257 |
# Supported by PyAtom: |
|
258 |
# XML Declaration: <?xml ... ?> |
|
259 |
# Comments: <!-- ... --> |
|
260 |
# Elements: <tag_name>...</tag_name> |
|
261 |
# |
|
262 |
# Minimal support: |
|
263 |
# Markup Declarations: <!KEYWORD ... > |
|
264 |
# Processing Instructions (PIs): <?KEYWORD ... ?> |
|
265 |
# |
|
266 |
# Not currently supported: |
|
267 |
# INCLUDE and IGNORE directives: <!KEYWORD[ ... ]]> |
|
268 |
# CDATA sections: <![CDATA[ ... ]]> |
|
269 |
# |
|
270 | ||
271 |
class XMLItem(object): |
|
272 |
""" |
|
273 |
All PyAtom classes inherit from this class. All it does is provide a |
|
274 |
few default methods, and be a root for the inheritance tree. |
|
275 | ||
276 |
An XMLItem has several methods that return an XML tag representation of |
|
277 |
its contents. Each XMLItem knows how to make a tag for itself. An |
|
278 |
XMLItem that contains other XMLItems will ask each one to make a tag; |
|
279 |
so asking the top-level XMLItem for a tag will cause the entire tree |
|
280 |
of XMLItems to recursively make tags, and you get a full XML |
|
281 |
representation with tags appropriately nested and indented. |
|
282 |
""" |
|
283 |
def _s_tag(self, tfc): |
|
284 |
""" |
|
285 |
A stub which must always be overridden by child classes. |
|
286 |
""" |
|
287 |
assert False, "XMLItem instance is too abstract to print." |
|
288 | ||
289 |
def s_tag(self, level): |
|
290 |
""" |
|
291 |
Return the item as a string containing an XML tag declaration. |
|
292 | ||
293 |
The XML tag will be indented. |
|
294 |
Will return an empty string if the item is empty. |
|
295 |
""" |
|
296 |
tfc = TFC(level, TFC.mode_normal) |
|
297 |
return self._s_tag(tfc) |
|
298 | ||
299 |
def s_tag_verbose(self, level): |
|
300 |
""" |
|
301 |
Return the item as a string containing an XML tag declaration. |
|
302 | ||
303 |
The XML tag will be indented. |
|
304 |
May return an XML Comment if the item is empty. |
|
305 |
""" |
|
306 |
tfc = TFC(level, TFC.mode_verbose) |
|
307 |
return self._s_tag(tfc) |
|
308 | ||
309 |
def s_tag_terse(self, level): |
|
310 |
""" |
|
311 |
Return the item as a string containing an XML tag declaration. |
|
312 | ||
313 |
The XML tag will not be indented. |
|
314 |
Will return an empty string if the item is empty. |
|
315 |
""" |
|
316 |
tfc = TFC(level, TFC.mode_terse) |
|
317 |
return self._s_tag(tfc) |
|
318 | ||
319 |
def __str__(self): |
|
320 |
return self.s_tag(0) |
|
321 | ||
322 |
def level(self): |
|
323 |
""" |
|
324 |
Return an integer describing what level this tag is. |
|
325 | ||
326 |
The root tag of an XML document is level 0; document-level comments |
|
327 |
or other document-level declarations are also level 0. Tags nested |
|
328 |
inside the root tag are level 1, tags nested inside those tags are |
|
329 |
level 2, and so on. |
|
330 | ||
331 |
This is currently only used by the s_tree() functions. When |
|
332 |
printing tags normally, the code that walks the tree keeps track of |
|
333 |
what level is current. |
|
334 |
""" |
|
335 |
level = 0 |
|
336 |
while self._parent != None: |
|
337 |
self = self._parent |
|
338 |
if self.is_element(): |
|
339 |
level += 1 |
|
340 |
return level |
|
341 | ||
342 |
def s_name(self): |
|
343 |
""" |
|
344 |
Return a name for the current item. |
|
345 | ||
346 |
Used only by the s_tree() functions. |
|
347 |
""" |
|
348 |
if self._name: |
|
349 |
return self._name |
|
350 |
return "unnamed_instance_of_" + type(self).__name__ |
|
351 | ||
352 |
def s_tree(self): |
|
353 |
""" |
|
354 |
Return a verbose tree showing the current tag and its children. |
|
355 | ||
356 |
This is for debugging; it's not valid XML syntax. |
|
357 |
""" |
|
358 |
level = self.level() |
|
359 |
return "%2d) %s\t%s" % (level, self.s_name(), str(self)) |
|
360 | ||
361 | ||
362 | ||
363 |
class DocItem(XMLItem): |
|
364 |
""" |
|
365 |
A document-level XML item (appearing above root element). |
|
366 | ||
367 |
Items that can be document-level inherit from this class. |
|
368 |
""" |
|
369 |
pass |
|
370 | ||
371 | ||
372 | ||
373 |
class ElementItem(XMLItem): |
|
374 |
""" |
|
375 |
An item that may be nested inside an element. |
|
376 | ||
377 |
Items that can be nested inside other elements inherit from this class. |
|
378 |
""" |
|
379 |
pass |
|
380 | ||
381 | ||
382 | ||
383 |
class Comment(DocItem,ElementItem): |
|
384 |
""" |
|
385 |
An XML comment. |
|
386 | ||
387 |
Attributes: |
|
388 |
text |
|
389 |
set the text of the comment |
|
390 |
""" |
|
391 |
def __init__(self, text=""): |
|
392 |
""" |
|
393 |
text: set the text of the comment |
|
394 |
""" |
|
395 |
self._parent = None |
|
396 |
self._name = "" |
|
397 |
self.tag_name = "comment" |
|
398 |
self.text = text |
|
399 | ||
400 |
def _s_tag(self, tfc): |
|
401 |
if not self: |
|
402 |
if tfc.b_print_all(): |
|
403 |
return tfc.s_indent() + "<!-- -->" |
|
404 |
else: |
|
405 |
return "" |
|
406 |
else: |
|
407 |
if self.text.find("\n") >= 0: |
|
408 |
lst = [] |
|
409 |
lst.append(tfc.s_indent() + "<!--") |
|
410 |
lst.append(self.text) |
|
411 |
lst.append(tfc.s_indent() + "-->") |
|
412 |
return "\n".join(lst) |
|
413 |
else: |
|
414 |
s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.text, " -->") |
|
415 |
return s |
|
416 | ||
417 |
assert False, "not possible to reach this line." |
|
418 | ||
419 |
def __nonzero__(self): |
|
420 |
# Returns True if there is any comment text. |
|
421 |
# Returns False otherwise. |
|
422 |
return not not self.text |
|
423 | ||
424 |
def is_element(self): |
|
425 |
return True |
|
426 | ||
427 | ||
428 | ||
429 |
# REVIEW: can a PI be an ElementItem? |
|
430 |
class PI(DocItem): |
|
431 |
""" |
|
432 |
XML Processing Instruction (PI). |
|
433 | ||
434 |
Attributes: |
|
435 |
keyword |
|
436 |
text |
|
437 |
""" |
|
438 |
def __init__(self): |
|
439 |
self._parent = None |
|
440 |
self._name = "" |
|
441 |
self.keyword = "" |
|
442 |
self.text = "" |
|
443 | ||
444 |
def _s_tag(self, tfc): |
|
445 |
if not self: |
|
446 |
return "" |
|
447 |
else: |
|
448 |
if self.text.find("\n") >= 0: |
|
449 |
lst = [] |
|
450 |
lst.append("%s%s%s" % (tfc.s_indent(), "<?", self.keyword)) |
|
451 |
lst.append(self.text) |
|
452 |
lst.append("%s%s" % (tfc.s_indent(), "?>")) |
|
453 |
return "\n".join(lst) |
|
454 |
else: |
|
455 |
s = "%s%s%s %s%s"% \ |
|
456 |
(tfc.s_indent(), "<?", self.keyword, self.text, "?>") |
|
457 |
return s |
|
458 | ||
459 |
assert False, "not possible to reach this line." |
|
460 | ||
461 |
def __nonzero__(self): |
|
462 |
# Returns True if there is any keyword. |
|
463 |
# Returns False otherwise. |
|
464 |
return not not self.keyword |
|
465 | ||
466 | ||
467 | ||
468 |
# REVIEW: can a MarkupDecl be an ElementItem? |
|
469 |
class MarkupDecl(DocItem): |
|
470 |
""" |
|
471 |
XML Markup Declaration. |
|
472 | ||
473 |
Attributes: |
|
474 |
keyword |
|
475 |
text |
|
476 |
""" |
|
477 |
def __init__(self): |
|
478 |
self._parent = None |
|
479 |
self._name = "" |
|
480 |
self.keyword = "" |
|
481 |
self.text = "" |
|
482 | ||
483 |
def _s_tag(self, tfc): |
|
484 |
if not self: |
|
485 |
return "" |
|
486 |
else: |
|
487 |
if self.text.find("\n") >= 0: |
|
488 |
lst = [] |
|
489 |
lst.append("%s%s%s" % (tfc.s_indent(), "<!", self.keyword)) |
|
490 |
lst.append(self.text) |
|
491 |
lst.append("%s%s" % (tfc.s_indent(), ">")) |
|
492 |
return "\n".join(lst) |
|
493 |
else: |
|
494 |
s = "%s%s%s %s%s" % \ |
|
495 |
(tfc.s_indent(), "<!", self.keyword, self.text, ">") |
|
496 |
return s |
|
497 | ||
498 |
assert False, "not possible to reach this line." |
|
499 | ||
500 |
def __nonzero__(self): |
|
501 |
# Returns True if there is any keyword. |
|
502 |
# Returns False otherwise. |
|
503 |
return not not self.keyword |
|
504 | ||
505 | ||
506 | ||
507 |
class CoreElement(ElementItem): |
|
508 |
""" |
|
509 |
This is an abstract class. |
|
510 | ||
511 |
All of the XML element classes inherit from this. |
|
512 |
""" |
|
513 |
def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []): |
|
514 |
# dictionary of attributes and their values |
|
515 |
self.lock = False |
|
516 |
self._parent = None |
|
517 |
self._name = "" |
|
518 |
self.tag_name = tag_name |
|
519 |
self.def_attr = def_attr |
|
520 |
self.attrs = {} |
|
521 |
if def_attr and def_attr_value: |
|
522 |
self.attrs[def_attr] = def_attr_value |
|
523 |
self.attr_names = attr_names |
|
524 |
self.lock = True |
|
525 | ||
526 |
def __nonzero__(self): |
|
527 |
# Returns True if any attrs are set or there are any contents. |
|
528 |
# Returns False otherwise. |
|
529 |
return not not self.attrs or self.has_contents() |
|
530 | ||
531 |
def text_check(self): |
|
532 |
""" |
|
533 |
Raise an exception, unless element has text contents. |
|
534 | ||
535 |
Child classes that have text must override this to do nothing. |
|
536 |
""" |
|
537 |
raise TypeError, "element does not have text contents" |
|
538 | ||
539 |
def nest_check(self): |
|
540 |
""" |
|
541 |
Raise an exception, unless element can nest other elements. |
|
542 | ||
543 |
Child classes that can nest must override this to do nothing. |
|
544 |
""" |
|
545 |
raise TypeError, "element cannot nest other elements" |
|
546 | ||
547 |
def __delattr__(self, name): |
|
548 |
# REVIEW: this should be made to work! |
|
549 |
raise TypeError, "cannot delete elements" |
|
550 | ||
551 |
def __getattr__(self, name): |
|
552 |
if name == "lock": |
|
553 |
# If the "lock" hasn't been created yet, we always want it |
|
554 |
# to be False, i.e. we are not locked. |
|
555 |
return False |
|
556 |
else: |
|
557 |
raise AttributeError, name |
|
558 | ||
559 |
def __setattr__(self, name, value): |
|
560 |
# Here's how this works: |
|
561 |
# |
|
562 |
# 0) "self.lock" is a boolean, set to False during __init__() |
|
563 |
# but turned True afterwards. When it's False, you can add new |
|
564 |
# members to the class instance without any sort of checks; once |
|
565 |
# it's set True, __setattr__() starts checking assignments. |
|
566 |
# By default, when lock is True, you cannot add a new member to |
|
567 |
# the class instance, and any assignment to an old member has to |
|
568 |
# be of matching type. So if you say "a.text = string", the |
|
569 |
# .text member has to exist and be a string member. |
|
570 |
# |
|
571 |
# This is the default __setattr__() for all element types. It |
|
572 |
# gets overloaded by the __setattr__() in NestElement, because |
|
573 |
# for nested elments, it makes sense to be able to add new |
|
574 |
# elements nested inside. |
|
575 |
# |
|
576 |
# This is moderately nice. But later in NestElement there is a |
|
577 |
# version of __setattr__() that is *very* nice; check it out. |
|
578 |
# |
|
579 |
# 1) This checks assignments to _parent, and makes sure they are |
|
580 |
# plausible (either an XMLItem, or None). |
|
581 | ||
582 |
try: |
|
583 |
lock = self.lock |
|
584 |
except AttributeError: |
|
585 |
lock = False |
|
586 | ||
587 |
if not lock: |
|
588 |
self.__dict__[name] = value |
|
589 |
return |
|
590 | ||
591 |
dict = self.__dict__ |
|
592 |
if not name in dict: |
|
593 |
# brand-new item |
|
594 |
if lock: |
|
595 |
raise TypeError, "element cannot nest other elements" |
|
596 | ||
597 |
if name == "_parent": |
|
598 |
if not (isinstance(value, XMLItem) or value is None): |
|
599 |
raise TypeError, "only XMLItem or None is permitted" |
|
600 |
self.__dict__[name] = value |
|
601 |
return |
|
602 | ||
603 |
# locked item so do checks |
|
604 |
if not type(self.__dict__[name]) is type(value): |
|
605 |
raise TypeError, "value is not the same type" |
|
606 | ||
607 |
self.__dict__[name] = value |
|
608 |
|
|
609 | ||
610 |
def has_contents(self): |
|
611 |
return False |
|
612 | ||
613 |
def multiline_contents(self): |
|
614 |
return False |
|
615 | ||
616 |
def s_contents(self, tfc): |
|
617 |
assert False, "CoreElement is an abstract class; it has no contents." |
|
618 | ||
619 |
def _s_start_tag_name_attrs(self, tfc): |
|
620 |
""" |
|
621 |
Return a string with the start tag name, and any attributes. |
|
622 | ||
623 |
Wrap this in correct punctuation to get a start tag. |
|
624 |
""" |
|
625 |
def attr_newline(tfc): |
|
626 |
if tfc.b_print_terse(): |
|
627 |
return " " |
|
628 |
else: |
|
629 |
return "\n" + tfc.s_indent(2) |
|
630 | ||
631 |
lst = [] |
|
632 |
lst.append(self.tag_name) |
|
633 | ||
634 |
if len(self.attrs) == 1: |
|
635 |
# just one attr so do on one line |
|
636 |
attr = self.attrs.keys()[0] |
|
637 |
s_attr = '%s="%s"' % (attr, self.attrs[attr]) |
|
638 |
lst.append(" " + s_attr) |
|
639 |
elif len(self.attrs) > 1: |
|
640 |
# more than one attr so do a nice nested tag |
|
641 |
# 0) show all attrs in the order of attr_names |
|
642 |
for attr in self.attr_names: |
|
643 |
if attr in self.attrs.keys(): |
|
644 |
s_attr = '%s="%s"' % (attr, self.attrs[attr]) |
|
645 |
lst.append(attr_newline(tfc) + s_attr) |
|
646 |
# 1) any attrs not in attr_names? list them, too |
|
647 |
for attr in self.attrs: |
|
648 |
if not attr in self.attr_names: |
|
649 |
s_attr = '%s="%s"' % (attr, self.attrs[attr]) |
|
650 |
lst.append(attr_newline(tfc) + s_attr) |
|
651 | ||
652 |
return "".join(lst) |
|
653 | ||
654 |
def _s_tag(self, tfc): |
|
655 |
if not self: |
|
656 |
if not tfc.b_print_all(): |
|
657 |
return "" |
|
658 | ||
659 |
lst = [] |
|
660 | ||
661 |
lst.append(tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc)) |
|
662 | ||
663 |
if not self.has_contents(): |
|
664 |
lst.append("/>") |
|
665 |
else: |
|
666 |
lst.append(">") |
|
667 |
if self.multiline_contents(): |
|
668 |
s = "\n%s\n" % self.s_contents(tfc.indent_by(1)) |
|
669 |
lst.append(s + tfc.s_indent()) |
|
670 |
else: |
|
671 |
lst.append(self.s_contents(tfc)) |
|
672 |
lst.append("</" + self.tag_name + ">") |
|
673 | ||
674 |
return "".join(lst) |
|
675 | ||
676 |
def s_start_tag(self, tfc): |
|
677 |
return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + ">" |
|
678 | ||
679 |
def s_end_tag(self): |
|
680 |
return "</" + self.tag_name + ">" |
|
681 | ||
682 |
def s_compact_tag(self, tfc): |
|
683 |
return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + "/>" |
|
684 | ||
685 |
def is_element(self): |
|
686 |
return True |
|
687 | ||
688 | ||
689 | ||
690 |
class TextElement(CoreElement): |
|
691 |
""" |
|
692 |
An element that cannot have other elements nested inside it. |
|
693 | ||
694 |
Attributes: |
|
695 |
attr |
|
696 |
text |
|
697 |
""" |
|
698 |
def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []): |
|
699 |
CoreElement.__init__(self, tag_name, def_attr, def_attr_value, |
|
700 |
attr_names) |
|
701 |
self.lock = False |
|
702 |
self.text = "" |
|
703 |
self.lock = True |
|
704 | ||
705 |
def text_check(self): |
|
706 |
pass |
|
707 | ||
708 |
def has_contents(self): |
|
709 |
return not not self.text |
|
710 | ||
711 |
def multiline_contents(self): |
|
712 |
return self.text.find("\n") >= 0 |
|
713 | ||
714 |
def s_contents(self, tfc): |
|
715 |
return self.text |
|
716 | ||
717 | ||
718 | ||
719 |
class Nest(ElementItem): |
|
720 |
""" |
|
721 |
A data structure that can store Elements, nested inside it. |
|
722 | ||
723 |
Note: this is not, itself, an Element! Because it is not an XML |
|
724 |
element, it has no tags. Its string representation is the |
|
725 |
representations of the elements nested inside it. |
|
726 | ||
727 |
NestElement and XMLDoc inherit from this. |
|
728 |
""" |
|
729 |
def __init__(self): |
|
730 |
self.lock = False |
|
731 |
self._parent = None |
|
732 |
self._name = "" |
|
733 |
self.elements = [] |
|
734 |
self.lock = True |
|
735 |
def __len__(self): |
|
736 |
return len(self.elements) |
|
737 |
def __getitem__(self, key): |
|
738 |
return self.elements[key] |
|
739 |
def __setitem__(self, key, value): |
|
740 |
self.elements[key] = value |
|
741 |
def __delitem__(self, key): |
|
742 |
del(self.elements[key]) |
|
743 | ||
744 |
def _do_setattr(self, name, value): |
|
745 |
if isinstance(value, XMLItem): |
|
746 |
value._parent = self |
|
747 |
value._name = name |
|
748 |
self.elements.append(value) |
|
749 |
self.__dict__[name] = value |
|
750 | ||
751 |
def __setattr__(self, name, value): |
|
752 |
# Lots of magic here! This is important stuff. Here's how it works: |
|
753 |
# |
|
754 |
# 0) self.lock is a boolean, set to False initially and then set |
|
755 |
# to True at the end of __init__(). When it's False, you can add new |
|
756 |
# members to the class instance without any sort of checks; once |
|
757 |
# it's set True, __setattr__() starts checking assignments. By |
|
758 |
# default, when lock is True, any assignment to an old member |
|
759 |
# has to be of matching type. You can add a new member to the |
|
760 |
# class instance, but __setattr__() checks to ensure that the |
|
761 |
# new member is an XMLItem. |
|
762 |
# |
|
763 |
# 1) Whether self.lock is set or not, if the value is an XMLitem, |
|
764 |
# then this will properly add the XMLItem into the tree |
|
765 |
# structure. The XMLItem will have _parent set to the parent, |
|
766 |
# will have _name set to its name in the parent, and will be |
|
767 |
# added to the parent's elements list. This is handled by |
|
768 |
# _do_setattr(). |
|
769 |
# |
|
770 |
# 2) As a convenience for the user, if the user is assigning a |
|
771 |
# string, and self is an XMLItem that has a .text value, this |
|
772 |
# will assign the string to the .text value. This allows usages |
|
773 |
# like "e.title = string", which is very nice. Before I added |
|
774 |
# this, I frequently wrote that instead of "e.title.text = |
|
775 |
# string" so I wanted it to just work. Likewise the user can |
|
776 |
# assign a time value directly into Timestamp elements. |
|
777 |
# |
|
778 |
# 3) This checks assignments to _parent, and makes sure they are |
|
779 |
# plausible (either an XMLItem, or None). |
|
780 | ||
781 |
try: |
|
782 |
lock = self.lock |
|
783 |
except AttributeError: |
|
784 |
lock = False |
|
785 | ||
786 |
if not lock: |
|
787 |
self._do_setattr(name, value) |
|
788 |
return |
|
789 | ||
790 |
dict = self.__dict__ |
|
791 |
if not name in dict: |
|
792 |
# brand-new item |
|
793 |
if lock: |
|
794 |
self.nest_check() |
|
795 |
if not isinstance(value, XMLItem): |
|
796 |
raise TypeError, "only XMLItem is permitted" |
|
797 |
self._do_setattr(name, value) |
|
798 |
return |
|
799 | ||
800 |
if name == "_parent" or name == "root_element": |
|
801 |
if not (isinstance(value, XMLItem) or value is None): |
|
802 |
raise TypeError, "only XMLItem or None is permitted" |
|
803 |
self.__dict__[name] = value |
|
804 |
return |
|
805 | ||
806 |
if name == "_name" and type(value) == type(""): |
|
807 |
self.__dict__[name] = value |
|
808 |
return |
|
809 | ||
810 |
# for Timestamp elements, allow this: element = time |
|
811 |
# (where "time" is a float value, since uses float for times) |
|
812 |
# Also allow valid timestamp strings. |
|
813 |
if isinstance(self.__dict__[name], Timestamp): |
|
814 |
if type(value) == type(1.0): |
|
815 |
self.__dict__[name].time = value |
|
816 |
return |
|
817 |
elif type(value) == type(""): |
|
818 |
t = utc_time_from_s_timestamp(value) |
|
819 |
if t: |
|
820 |
self.__dict__[name].time = t |
|
821 |
else: |
|
822 |
raise ValueError, "value must be a valid timestamp string" |
|
823 |
return |
|
824 | ||
825 |
# Allow string assignment to go to the .text attribute, for |
|
826 |
# elements that allow it. All TextElements allow it; |
|
827 |
# Elements will allow it if they do not nave nested elements. |
|
828 |
# text_check() raises an error if it's not allowed. |
|
829 |
if isinstance(self.__dict__[name], CoreElement) and \ |
|
830 |
type(value) == type(""): |
|
831 |
self.__dict__[name].text_check() |
|
832 |
self.__dict__[name].text = value |
|
833 |
return |
|
834 | ||
835 |
# locked item so do checks |
|
836 |
if not type(self.__dict__[name]) is type(value): |
|
837 |
raise TypeError, "value is not the same type" |
|
838 | ||
839 |
self.__dict__[name] = value |
|
840 |
|
|
841 |
def __delattr__(self, name): |
|
842 |
# This won't be used often, if ever, but if anyone tries it, it |
|
843 |
# should work. |
|
844 |
if isinstance(self.name, XMLItem): |
|
845 |
o = self.__dict__[name] |
|
846 |
self.elements.remove(o) |
|
847 |
del(self.__dict__[name]) |
|
848 |
else: |
|
849 |
# REVIEW: what error should this raise? |
|
850 |
raise TypeError, "cannot delete that item" |
|
851 | ||
852 |
def nest_check(self): |
|
853 |
pass |
|
854 | ||
855 |
def is_element(self): |
|
856 |
# a Nest is not really an element |
|
857 |
return False |
|
858 | ||
859 |
def has_contents(self): |
|
860 |
for element in self.elements: |
|
861 |
if element: |
|
862 |
return True |
|
863 |
# empty iff all of the elements were empty |
|
864 |
return False |
|
865 | ||
866 |
def __nonzero__(self): |
|
867 |
return self.has_contents() |
|
868 | ||
869 |
def multiline_contents(self): |
|
870 |
# if there are any contents, we want multiline for nested tags |
|
871 |
return self.has_contents() |
|
872 | ||
873 |
def s_contents(self, tfc): |
|
874 |
if len(self.elements) > 0: |
|
875 |
# if any nested elements exist, we show those |
|
876 |
lst = [] |
|
877 | ||
878 |
for element in self.elements: |
|
879 |
s = element._s_tag(tfc) |
|
880 |
if s: |
|
881 |
lst.append(s) |
|
882 | ||
883 |
return "\n".join(lst) |
|
884 |
else: |
|
885 |
return "" |
|
886 | ||
887 |
assert False, "not possible to reach this line." |
|
888 |
return "" |
|
889 | ||
890 |
def s_tree(self): |
|
891 |
level = self.level() |
|
892 |
tup = (level, self.s_name(), self.__class__.__name__) |
|
893 |
s = "%2d) %s (instance of %s)" % tup |
|
894 |
lst = [] |
|
895 |
lst.append(s) |
|
896 |
for element in self.elements: |
|
897 |
s = element.s_tree() |
|
898 |
lst.append(s) |
|
899 |
return "\n".join(lst) |
|
900 | ||
901 |
def _s_tag(self, tfc): |
|
902 |
return self.s_contents(tfc) |
|
903 | ||
904 | ||
905 | ||
906 | ||
907 |
class NestElement(Nest,CoreElement): |
|
908 |
""" |
|
909 |
An element that can have other elements nested inside it. |
|
910 | ||
911 |
Attributes: |
|
912 |
attr |
|
913 |
elements: a list of other elements nested inside this one. |
|
914 |
""" |
|
915 |
def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]): |
|
916 |
CoreElement.__init__(self, tag_name, def_attr, def_attr_value, |
|
917 |
attr_names) |
|
918 |
self.lock = False |
|
919 |
self.elements = [] |
|
920 |
self.lock = True |
|
921 | ||
922 |
def is_element(self): |
|
923 |
return True |
|
924 | ||
925 |
def __nonzero__(self): |
|
926 |
return CoreElement.__nonzero__(self) |
|
927 | ||
928 |
def _s_tag(self, tfc): |
|
929 |
return CoreElement._s_tag(self, tfc) |
|
930 | ||
931 | ||
932 | ||
933 |
class Element(NestElement,TextElement): |
|
934 |
""" |
|
935 |
A class to represent an arbitrary XML tag. Can either have other XML |
|
936 |
elements nested inside it, or else can have a text string value, but |
|
937 |
never both at the same time. |
|
938 | ||
939 |
This is intended for user-defined XML tags. The user can just use |
|
940 |
"Element" for all custom tags. |
|
941 | ||
942 |
PyAtom doesn't use this; PyAtom uses TextElement for tags with a text |
|
943 |
string value, and NestElement for tags that nest other elements. Users |
|
944 |
can do the same, or can just use Element, as they like. |
|
945 | ||
946 |
Attributes: |
|
947 |
attr |
|
948 |
elements: a list of other elements nested inside, if any |
|
949 |
text: a text string value, if any |
|
950 | ||
951 |
Note: if text is set, elements will be empty, and vice-versa. If you |
|
952 |
have elements nested inside and try to set the .text, this will raise |
|
953 |
an exception, and vice-versa. |
|
954 |
""" |
|
955 |
# A Element can have other elements nested inside it, or it can have |
|
956 |
# a single ".text" string value. But never both at the same time. |
|
957 |
# Once you nest another element, you can no longer use the .text. |
|
958 |
def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]): |
|
959 |
NestElement.__init__(self, tag_name, def_attr, def_attr_value, |
|
960 |
attr_names) |
|
961 |
self.lock = False |
|
962 |
self.text = "" |
|
963 |
self.lock = True |
|
964 | ||
965 |
def nest_check(self): |
|
966 |
if self.text: |
|
967 |
raise TypeError, "Element has text contents so cannot nest" |
|
968 | ||
969 |
def text_check(self): |
|
970 |
if len(self.elements) > 0: |
|
971 |
raise TypeError, "Element has nested elements so cannot assign text" |
|
972 | ||
973 |
def has_contents(self): |
|
974 |
return NestElement.has_contents(self) or TextElement.has_contents(self) |
|
975 | ||
976 |
def multiline_contents(self): |
|
977 |
return NestElement.has_contents(self) or self.text.find("\n") >= 0 |
|
978 | ||
979 |
def s_contents(self, tfc): |
|
980 |
if len(self.elements) > 0: |
|
981 |
return NestElement.s_contents(self, tfc) |
|
982 |
elif self.text: |
|
983 |
return TextElement.s_contents(self, tfc) |
|
984 |
else: |
|
985 |
return "" |
|
986 |
assert False, "not possible to reach this line." |
|
987 | ||
988 |
def s_tree(self): |
|
989 |
lst = [] |
|
990 |
if len(self.elements) > 0: |
|
991 |
level = self.level() |
|
992 |
tup = (level, self.s_name(), self.__class__.__name__) |
|
993 |
s = "%2d) %s (instance of %s)" % tup |
|
994 |
lst.append(s) |
|
995 |
for element in self.elements: |
|
996 |
s = element.s_tree() |
|
997 |
lst.append(s) |
|
998 |
return "\n".join(lst) |
|
999 |
elif self.text: |
|
1000 |
return XMLItem.s_tree(self) |
|
1001 |
else: |
|
1002 |
level = self.level() |
|
1003 |
tfc = TFC(level) |
|
1004 |
s = "%2d) %s %s" % (level, self.s_name(), "empty Element...") |
|
1005 |
return s |
|
1006 |
assert False, "not possible to reach this line." |
|
1007 | ||
1008 | ||
1009 | ||
1010 |
class Collection(XMLItem): |
|
1011 |
""" |
|
1012 |
A Collection contains 0 or more Elements, but isn't an XML element. |
|
1013 |
Use where a run of 0 or more Elements of the same type is legal. |
|
1014 | ||
1015 |
When you init your Collection, you specify what class of Element it will |
|
1016 |
contain. Attempts to append an Element of a different class will raise |
|
1017 |
an exception. Note, however, that the various Element classes all |
|
1018 |
inherit from base classes, and you can specify a class from higher up in |
|
1019 |
the inheritance tree. You could, if you wanted, make a Collection |
|
1020 |
containing "XMLItem" and then any item defined in PyAtom would be legal |
|
1021 |
in that collection. (See XMLDoc, which contains two collections of |
|
1022 |
DocItem.) |
|
1023 | ||
1024 |
Attributes: |
|
1025 |
contains: the class of element this Collection will contain |
|
1026 |
elements: a list of other elements nested inside, if any |
|
1027 | ||
1028 |
Note: The string representation of a Collection is just the string |
|
1029 |
representations of the elements inside it. However, a verbose string |
|
1030 |
reprentation may have an XML comment like this: |
|
1031 | ||
1032 |
<!-- Collection of <class> with <n> elements --> |
|
1033 | ||
1034 |
where <n> is the number of elements in the Collection and <class> is the |
|
1035 |
name of the class in this Collection. |
|
1036 |
""" |
|
1037 |
def __init__(self, element_class): |
|
1038 |
self.lock = False |
|
1039 |
self._parent = None |
|
1040 |
self._name = "" |
|
1041 |
self.elements = [] |
|
1042 |
self.contains = element_class |
|
1043 |
self.lock = True |
|
1044 |
def __len__(self): |
|
1045 |
return len(self.elements) |
|
1046 |
def __getitem__(self, key): |
|
1047 |
return self.elements[key] |
|
1048 |
def __setitem__(self, key, value): |
|
1049 |
if not isinstance(value, self.contains): |
|
1050 |
raise TypeError, "object is the wrong type for this collection" |
|
1051 |
self.elements[key] = value |
|
1052 |
def __delitem__(self, key): |
|
1053 |
del(self.elements[key]) |
|
1054 | ||
1055 |
def __nonzero__(self): |
|
1056 |
# there are no attrs so if any element is nonzero, collection is too |
|
1057 |
for element in self.elements: |
|
1058 |
if element: |
|
1059 |
return True |
|
1060 |
return False |
|
1061 | ||
1062 |
def is_element(self): |
|
1063 |
# A Collection is not really an Element |
|
1064 |
return False |
|
1065 | ||
1066 |
def s_coll(self): |
|
1067 |
name = self.contains.__name__ |
|
1068 |
n = len(self.elements) |
|
1069 |
if n == 1: |
|
1070 |
el = "element" |
|
1071 |
else: |
|
1072 |
el = "elements" |
|
1073 |
return "collection of %s with %d %s" % (name, n, el) |
|
1074 | ||
1075 |
def append(self, element): |
|
1076 |
if not isinstance(element, self.contains): |
|
1077 |
print >> sys.stderr, "Error: attempted to insert", \ |
|
1078 |
type(element).__name__, \ |
|
1079 |
"into collection of", self.contains.__name__ |
|
1080 |
raise TypeError, "object is the wrong type for this collection" |
|
1081 |
element._parent = self |
|
1082 |
self.elements.append(element) |
|
1083 | ||
1084 |
def _s_tag(self, tfc): |
|
1085 |
# A collection exists only as a place to put real elements. |
|
1086 |
# There are no start or end tags... |
|
1087 |
# When tfc.b_print_all() is true, we do put an XML comment. |
|
1088 | ||
1089 |
if not self.elements: |
|
1090 |
if not tfc.b_print_all(): |
|
1091 |
return "" |
|
1092 | ||
1093 |
lst = [] |
|
1094 | ||
1095 |
if tfc.b_print_verbose(): |
|
1096 |
s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.s_coll(), " -->") |
|
1097 |
lst.append(s) |
|
1098 |
tfc = tfc.indent_by(1) |
|
1099 | ||
1100 |
for element in self.elements: |
|
1101 |
s = element._s_tag(tfc) |
|
1102 |
if s: |
|
1103 |
lst.append(s) |
|
1104 | ||
1105 |
return "\n".join(lst) |
|
1106 | ||
1107 |
def s_tree(self): |
|
1108 |
level = self.level() |
|
1109 |
s = "%2d) %s %s" % (level, self.s_name(), self.s_coll()) |
|
1110 |
lst = [] |
|
1111 |
lst.append(s) |
|
1112 |
for element in self.elements: |
|
1113 |
s = element.s_tree() |
|
1114 |
lst.append(s) |
|
1115 |
return "\n".join(lst) |
|
1116 | ||
1117 | ||
1118 | ||
1119 |
class XMLDeclaration(XMLItem): |
|
1120 |
# REVIEW: should this print multi-line for multiple attrs? |
|
1121 |
def __init__(self): |
|
1122 |
self._parent = None |
|
1123 |
self._name = "" |
|
1124 |
self.attrs = {} |
|
1125 |
self.attrs[s_version] = "1.0" |
|
1126 |
self.attrs[s_encoding] = "utf-8" |
|
1127 |
self.attr_names = [s_version, s_encoding, s_standalone] |
|
1128 | ||
1129 |
def _s_tag(self, tfc): |
|
1130 |
# An XMLDeclaration() instance is never empty, so always prints. |
|
1131 | ||
1132 |
lst = [] |
|
1133 |
s = "%s%s" % (tfc.s_indent(), "<?xml") |
|
1134 |
lst.append(s) |
|
1135 |
# 0) show all attrs in the order of attr_names |
|
1136 |
for attr in self.attr_names: |
|
1137 |
if attr in self.attrs.keys(): |
|
1138 |
s_attr = ' %s="%s"' % (attr, self.attrs[attr]) |
|
1139 |
lst.append(s_attr) |
|
1140 |
# 1) any attrs not in attr_names? list them, too |
|
1141 |
for attr in self.attrs: |
|
1142 |
if not attr in self.attr_names: |
|
1143 |
s_attr = ' %s="%s"' % (attr, self.attrs[attr]) |
|
1144 |
lst.append(s_attr) |
|
1145 |
lst.append("?>") |
|
1146 | ||
1147 |
return "".join(lst) |
|
1148 | ||
1149 |
def __nonzero__(self): |
|
1150 |
# Returns True because the XML Declaration is never empty. |
|
1151 |
return True |
|
1152 | ||
1153 |
def is_element(self): |
|
1154 |
return True |
|
1155 | ||
1156 | ||
1157 | ||
1158 |
class XMLDoc(Nest): |
|
1159 |
""" |
|
1160 |
A data structure to represent an XML Document. It will have the |
|
1161 |
following structure: |
|
1162 | ||
1163 |
the XML Declaration item |
|
1164 |
0 or more document-level XML items |
|
1165 |
exactly one XML item (the "root tag") |
|
1166 |
0 or more document-level XML items |
|
1167 | ||
1168 |
document level XML items are: Comment, PI, MarkupDecl |
|
1169 | ||
1170 | ||
1171 |
Attributes: |
|
1172 |
xml_decl: the XMLDeclaration item |
|
1173 |
docitems_above: a collection of DocItem (items above root_element) |
|
1174 |
root_element: the XML tag containing your data |
|
1175 |
docitems_below: a collection of DocItem (items below root_element) |
|
1176 | ||
1177 |
Note: usually the root_element has lots of other XML items nested inside |
|
1178 |
it! |
|
1179 |
""" |
|
1180 |
def __init__(self, root_element=None): |
|
1181 |
Nest.__init__(self) |
|
1182 | ||
1183 |
self._name = "XMLDoc" |
|
1184 | ||
1185 |
self.xml_decl = XMLDeclaration() |
|
1186 |
self.docitems_above = Collection(DocItem) |
|
1187 | ||
1188 |
if not root_element: |
|
1189 |
root_element = Comment("no root element yet") |
|
1190 |
self.root_element = root_element |
|
1191 | ||
1192 |
self.docitems_below = Collection(DocItem) |
|
1193 | ||
1194 |
def __setattr__(self, name, value): |
|
1195 |
# root_element may always be set to any ElementItem |
|
1196 |
if name == "root_element": |
|
1197 |
if not (isinstance(value, ElementItem)): |
|
1198 |
raise TypeError, "only ElementItem is permitted" |
|
1199 | ||
1200 |
self.lock = False |
|
1201 |
# Item checks out, so assign it. root_element should only |
|
1202 |
# ever have one element, and we always put the new element |
|
1203 |
# in the same slot in elements[]. |
|
1204 |
if "i_root_element" in self.__dict__: |
|
1205 |
# Assign new root_element over old one in elements[] |
|
1206 |
assert self.elements[self.i_root_element] == self.root_element |
|
1207 |
self.elements[self.i_root_element] = value |
|
1208 |
else: |
|
1209 |
# This is the first time root_element was ever set. |
|
1210 |
self.i_root_element = len(self.elements) |
|
1211 |
self.elements.append(value) |
|
1212 | ||
1213 |
value._parent = self |
|
1214 |
value._name = name |
|
1215 |
self.__dict__[name] = value |
|
1216 |
self.lock = True |
|
1217 |
else: |
|
1218 |
# for all other, fall through to inherited behavior |
|
1219 |
Nest.__setattr__(self, name, value) |
|
1220 | ||
1221 |
def Validate(self): |
|
1222 |
# XMLDoc never has parent. Never change this! |
|
1223 |
assert self._parent == None |
|
1224 |
return True |
|
1225 | ||
1226 | ||
1227 | ||
1228 |
def local_time_from_utc_time(t): |
|
1229 |
return t - time.timezone |
|
1230 | ||
1231 |
def utc_time_from_local_time(t): |
|
1232 |
return t + time.timezone |
|
1233 | ||
1234 |
def local_time(): |
|
1235 |
return time.time() - time.timezone |
|
1236 | ||
1237 |
def utc_time(): |
|
1238 |
return time.time() |
|
1239 | ||
1240 | ||
1241 |
class TimeSeq(object): |
|
1242 |
""" |
|
1243 |
A class to generate a sequence of timestamps. |
|
1244 | ||
1245 |
Atom feed validators complain if multiple timestamps have the same |
|
1246 |
value, so this provides a convenient way to set a bunch of timestamps |
|
1247 |
all at least one second different from each other. |
|
1248 |
""" |
|
1249 |
def __init__(self, init_time=0): |
|
1250 |
if init_time == 0: |
|
1251 |
self.time = local_time() |
|
1252 |
else: |
|
1253 |
self.time = float(init_time) |
|
1254 |
def next(self): |
|
1255 |
t = self.time |
|
1256 |
self.time += 1.0 |
|
1257 |
return t |
|
1258 | ||
1259 |
format_RFC3339 = "%Y-%m-%dT%H:%M:%S" |
|
1260 | ||
1261 |
def parse_time_offset(s): |
|
1262 |
s = s.lstrip().rstrip() |
|
1263 | ||
1264 |
if (s == '' or s == 'Z' or s == 'z'): |
|
1265 |
return 0 |
|
1266 | ||
1267 |
m = pat_time_offset.search(s) |
|
1268 |
sign = m.group(1) |
|
1269 |
offset_hour = int(m.group(2)) |
|
1270 |
offset_min = int(m.group(3)) |
|
1271 |
offset = offset_hour * 3600 + offset_min * 60 |
|
1272 |
if sign == "-": |
|
1273 |
offset *= -1 |
|
1274 |
return offset |
|
1275 | ||
1276 |
def s_timestamp(utc_time, time_offset="Z"): |
|
1277 |
""" |
|
1278 |
Format a time and offset into a string. |
|
1279 | ||
1280 |
utc_time |
|
1281 |
a floating-point value, time in the UTC time zone |
|
1282 |
s_time_offset |
|
1283 |
a string specifying an offset from UTC. Examples: |
|
1284 |
z or Z -- offset is 0 ("Zulu" time, UTC, aka GMT) |
|
1285 |
-08:00 -- 8 hours earlier than UTC (Pacific time zone) |
|
1286 |
"" -- empty string is technically not legal, but may work |
|
1287 | ||
1288 |
Notes: |
|
1289 |
Returned string complies with RFC3339; uses ISO8601 date format. |
|
1290 |
Example: 2003-12-13T18:30:02Z |
|
1291 |
Example: 2003-12-13T18:30:02+02:00 |
|
1292 |
""" |
|
1293 | ||
1294 |
if not utc_time: |
|
1295 |
return "" |
|
1296 | ||
1297 |
utc_time += parse_time_offset(time_offset) |
|
1298 | ||
1299 |
try: |
|
1300 |
s = time.strftime(format_RFC3339, time.localtime(utc_time)) |
|
1301 |
except: |
|
1302 |
return "" |
|
1303 | ||
1304 |
return s + time_offset |
|
1305 | ||
1306 | ||
1307 | ||
1308 |
pat_RFC3339 = re.compile("(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(.*)") |
|
1309 |
pat_time_offset = re.compile("([+-])(\d\d):(\d\d)") |
|
1310 | ||
1311 |
def utc_time_from_s_timestamp(s_date_time_stamp): |
|
1312 |
# parse RFC3339-compatible times that use ISO8601 date format |
|
1313 |
# date time stamp example: 2003-12-13T18:30:02Z |
|
1314 |
# date time stamp example: 2003-12-13T18:30:02+02:00 |
|
1315 |
# leaving off the suffix is technically not legal, but allowed |
|
1316 | ||
1317 |
s_date_time_stamp = s_date_time_stamp.lstrip().rstrip() |
|
1318 | ||
1319 |
try: |
|
1320 |
m = pat_RFC3339.search(s_date_time_stamp) |
|
1321 |
year = int(m.group(1)) |
|
1322 |
mon = int(m.group(2)) |
|
1323 |
mday = int(m.group(3)) |
|
1324 |
hour = int(m.group(4)) |
|
1325 |
min = int(m.group(5)) |
|
1326 |
sec = int(m.group(6)) |
|
1327 |
tup = (year, mon, mday, hour, min, sec, -1, -1, -1) |
|
1328 |
t = time.mktime(tup) |
|
1329 | ||
1330 |
s = m.group(7) |
|
1331 |
t += parse_time_offset(s) |
|
1332 | ||
1333 |
return t |
|
1334 | ||
1335 |
except: |
|
1336 |
return 0.0 |
|
1337 | ||
1338 |
assert False, "impossible to reach this line" |
|
1339 | ||
1340 | ||
1341 |
def s_time_offset(): |
|
1342 |
""" |
|
1343 |
Return a string with local offset from UTC in RFC3339 format. |
|
1344 |
""" |
|
1345 | ||
1346 |
# If t is set to local time in seconds since the epoch, then... |
|
1347 |
# ...offset is the value you add to t to get UTC. This is the |
|
1348 |
# reverse of time.timezone. |
|
1349 | ||
1350 |
offset = -(time.timezone) |
|
1351 | ||
1352 |
if offset > 0: |
|
1353 |
sign = "+" |
|
1354 |
else: |
|
1355 |
sign = "-" |
|
1356 |
offset = abs(offset) |
|
1357 | ||
1358 |
offset_hour = offset // (60 * 60) |
|
1359 |
offset_min = (offset // 60) % 60 |
|
1360 |
return "%s%02d:%02d" % (sign, offset_hour, offset_min) |
|
1361 | ||
1362 |
s_offset_local = s_time_offset() |
|
1363 | ||
1364 |
s_offset_default = s_offset_local |
|
1365 | ||
1366 |
def set_default_time_offset(s): |
|
1367 |
global s_offset_default |
|
1368 |
s_offset_default = s |
|
1369 | ||
1370 | ||
1371 |
class Timestamp(CoreElement): |
|
1372 |
def __init__(self, tag_name, time=0.0): |
|
1373 |
CoreElement.__init__(self, tag_name, None, None) |
|
1374 |
self.lock = False |
|
1375 |
self.time = time |
|
1376 |
self.time_offset = s_offset_default |
|
1377 |
self.lock = True |
|
1378 | ||
1379 |
def __delattr__(self, name): |
|
1380 |
CoreElement.__delattr_(self, name) |
|
1381 | ||
1382 |
def __getattr__(self, name): |
|
1383 |
if name == "text": |
|
1384 |
return s_timestamp(self.time, self.time_offset) |
|
1385 |
return CoreElement.__getattr_(self, name) |
|
1386 | ||
1387 |
def __setattr__(self, name, value): |
|
1388 |
if name == "text": |
|
1389 |
if type(value) != type(""): |
|
1390 |
raise TypeError, "can only assign a string to .text" |
|
1391 |
t = utc_time_from_s_timestamp(value) |
|
1392 |
if t: |
|
1393 |
self.time = utc_time_from_s_timestamp(value) |
|
1394 |
else: |
|
1395 |
raise ValueError, "value must be a valid timestamp string" |
|
1396 |
return |
|
1397 |
CoreElement.__setattr__(self, name, value) |
|
1398 | ||
1399 |
def has_contents(self): |
|
1400 |
return self.time != 0 |
|
1401 | ||
1402 |
def multiline_contents(self): |
|
1403 |
return False |
|
1404 | ||
1405 |
def s_contents(self, tfc): |
|
1406 |
return s_timestamp(self.time, self.time_offset) |
|
1407 | ||
1408 |
def update(self): |
|
1409 |
self.time = local_time() |
|
1410 |
return self |
|
1411 | ||
1412 | ||
1413 | ||
1414 | ||
1415 |
# Below are all the classes to implement Atom using the above tools. |
|
1416 | ||
1417 | ||
1418 | ||
1419 |
class AtomText(TextElement): |
|
1420 |
def __init__(self, tag_name): |
|
1421 |
attr_names = [ s_type ] |
|
1422 |
# legal values of type: "text", "html", "xhtml" |
|
1423 |
TextElement.__init__(self, tag_name, None, None, attr_names) |
|
1424 | ||
1425 |
class Title(AtomText): |
|
1426 |
def __init__(self, text=""): |
|
1427 |
AtomText.__init__(self, "title") |
|
1428 |
self.text = text |
|
1429 |
|
|
1430 |
class Subtitle(AtomText): |
|
1431 |
def __init__(self, text=""): |
|
1432 |
AtomText.__init__(self, "subtitle") |
|
1433 |
self.text = text |
|
1434 |
|
|
1435 |
class Content(AtomText): |
|
1436 |
def __init__(self, text=""): |
|
1437 |
AtomText.__init__(self, "content") |
|
1438 |
self.text = text |
|
1439 |
|
|
1440 |
class Summary(AtomText): |
|
1441 |
def __init__(self, text=""): |
|
1442 |
AtomText.__init__(self, "summary") |
|
1443 |
self.text = text |
|
1444 |
|
|
1445 |
class Rights(AtomText): |
|
1446 |
def __init__(self, text=""): |
|
1447 |
AtomText.__init__(self, "rights") |
|
1448 |
self.text = text |
|
1449 |
|
|
1450 |
class Id(TextElement): |
|
1451 |
def __init__(self, text=""): |
|
1452 |
TextElement.__init__(self, "id", None, None) |
|
1453 |
self.text = text |
|
1454 |
|
|
1455 |
class Generator(TextElement): |
|
1456 |
def __init__(self): |
|
1457 |
attr_names = [ "uri", "version" ] |
|
1458 |
TextElement.__init__(self, "generator", None, None, attr_names) |
|
1459 |
|
|
1460 |
class Category(TextElement): |
|
1461 |
def __init__(self, term_val=""): |
|
1462 |
attr_names = [s_term, "scheme", "label"] |
|
1463 |
TextElement.__init__(self, "category", s_term, term_val, attr_names) |
|
1464 | ||
1465 |
class Link(TextElement): |
|
1466 |
def __init__(self, href_val=""): |
|
1467 |
attr_names = [ |
|
1468 |
s_href, "rel", "type", "hreflang", "title", "length", s_lang] |
|
1469 |
TextElement.__init__(self, "link", s_href, href_val, attr_names) |
|
1470 | ||
1471 |
class Icon(TextElement): |
|
1472 |
def __init__(self): |
|
1473 |
TextElement.__init__(self, "icon", None, None) |
|
1474 | ||
1475 |
class Logo(TextElement): |
|
1476 |
def __init__(self): |
|
1477 |
TextElement.__init__(self, "logo", None, None) |
|
1478 | ||
1479 |
class Name(TextElement): |
|
1480 |
def __init__(self, text=""): |
|
1481 |
TextElement.__init__(self, "name", None, None) |
|
1482 |
self.text = text |
|
1483 | ||
1484 |
class Email(TextElement): |
|
1485 |
def __init__(self): |
|
1486 |
TextElement.__init__(self, "email", None, None) |
|
1487 | ||
1488 |
class Uri(TextElement): |
|
1489 |
def __init__(self): |
|
1490 |
TextElement.__init__(self, "uri", None, None) |
|
1491 | ||
1492 | ||
1493 | ||
1494 |
class BasicAuthor(NestElement): |
|
1495 |
def __init__(self, tag_name, name): |
|
1496 |
NestElement.__init__(self, tag_name, None, None) |
|
1497 |
self.name = Name(name) |
|
1498 |
self.email = Email() |
|
1499 |
self.uri = Uri() |
|
1500 | ||
1501 |
class Author(BasicAuthor): |
|
1502 |
def __init__(self, name=""): |
|
1503 |
BasicAuthor.__init__(self, "author", name) |
|
1504 | ||
1505 |
class Contributor(BasicAuthor): |
|
1506 |
def __init__(self, name=""): |
|
1507 |
BasicAuthor.__init__(self, "contributor", name) |
|
1508 | ||
1509 | ||
1510 | ||
1511 |
class Updated(Timestamp): |
|
1512 |
def __init__(self, time=0.0): |
|
1513 |
Timestamp.__init__(self, "updated", time) |
|
1514 | ||
1515 |
class Published(Timestamp): |
|
1516 |
def __init__(self, time=0.0): |
|
1517 |
Timestamp.__init__(self, "published", time) |
|
1518 | ||
1519 | ||
1520 | ||
1521 |
class FeedElement(NestElement): |
|
1522 |
def __init__(self, tag_name): |
|
1523 |
NestElement.__init__(self, tag_name, None, None) |
|
1524 | ||
1525 |
self.title = Title("") |
|
1526 |
self.id = Id("") |
|
1527 |
self.updated = Updated() |
|
1528 |
self.authors = Collection(Author) |
|
1529 |
self.links = Collection(Link) |
|
1530 | ||
1531 |
self.subtitle = Subtitle("") |
|
1532 |
self.categories = Collection(Category) |
|
1533 |
self.contributors = Collection(Contributor) |
|
1534 |
self.generator = Generator() |
|
1535 |
self.icon = Icon() |
|
1536 |
self.logo = Logo() |
|
1537 |
self.rights = Rights("") |
|
1538 | ||
1539 |
class Feed(FeedElement): |
|
1540 |
def __init__(self): |
|
1541 |
FeedElement.__init__(self, "feed") |
|
1542 |
self.attrs["xmlns"] = "http://www.w3.org/2005/Atom" |
|
1543 |
self.title.text = "Title of Feed Goes Here" |
|
1544 |
self.id.text = "ID of Feed Goes Here" |
|
1545 |
self.entries = Collection(Entry) |
|
1546 | ||
1547 |
class Source(FeedElement): |
|
1548 |
def __init__(self): |
|
1549 |
FeedElement.__init__(self, "source") |
|
1550 | ||
1551 | ||
1552 | ||
1553 |
class Entry(NestElement): |
|
1554 |
def __init__(self): |
|
1555 |
NestElement.__init__(self, "entry", None, None) |
|
1556 |
self.title = Title("Title of Entry Goes Here") |
|
1557 |
self.id = Id("ID of Entry Goes Here") |
|
1558 |
self.updated = Updated() |
|
1559 |
self.authors = Collection(Author) |
|
1560 |
self.links = Collection(Link) |
|
1561 | ||
1562 |
self.content = Content("") |
|
1563 |
self.summary = Summary("") |
|
1564 |
self.categories = Collection(Category) |
|
1565 |
self.contributors = Collection(Contributor) |
|
1566 |
self.published = Published() |
|
1567 |
self.source = Source() |
|
1568 |
self.rights = Rights("") |
|
1569 | ||
1570 | ||
1571 | ||
1572 |
def diff(s0, name0, s1, name1): |
|
1573 |
from difflib import ndiff |
|
1574 |
lst0 = s0.split("\n") |
|
1575 |
lst1 = s1.split("\n") |
|
1576 |
report = '\n'.join(ndiff(lst0, lst1)) |
|
1577 |
return report |
|
1578 | ||
1579 | ||
1580 |
def run_test_cases(): |
|
1581 | ||
1582 |
# The default is to make time stamps in local time with appropriate |
|
1583 |
# offset; for our tests, we want a default "Z" offset instead. |
|
1584 |
set_default_time_offset("Z") |
|
1585 | ||
1586 |
failed_tests = 0 |
|
1587 | ||
1588 | ||
1589 |
# Test: convert current time into a timestamp string and back |
|
1590 | ||
1591 |
now = local_time() |
|
1592 |
# timestamp format does not allow fractional seconds |
|
1593 |
now = float(int(now)) # truncate any fractional seconds |
|
1594 |
s = s_timestamp(now) |
|
1595 |
t = utc_time_from_s_timestamp(s) |
|
1596 |
if now != t: |
|
1597 |
failed_tests += 1 |
|
1598 |
print "test case failed:" |
|
1599 |
print now, "-- original timestamp" |
|
1600 |
print t, "-- converted timestamp does not match" |
|
1601 | ||
1602 | ||
1603 |
# Test: convert a timestamp string to a time value and back |
|
1604 | ||
1605 |
s_time = "2003-12-13T18:30:02Z" |
|
1606 |
t = utc_time_from_s_timestamp(s_time) |
|
1607 |
s = s_timestamp(t) |
|
1608 |
if s_time != s: |
|
1609 |
failed_tests += 1 |
|
1610 |
print "test case failed:" |
|
1611 |
print s_time, "-- original timestamp" |
|
1612 |
print s, "-- converted timestamp does not match" |
|
1613 | ||
1614 | ||
1615 |
# Test: generate the "Atom-Powered Robots Run Amok" example |
|
1616 |
# |
|
1617 |
# Note: the original had some of the XML declarations in |
|
1618 |
# a different order than PyAtom puts them. I swapped around |
|
1619 |
# the lines here so they would match the PyAtom order. Other |
|
1620 |
# than that, this is the example from: |
|
1621 |
# |
|
1622 |
# http://www.atomenabled.org/developers/syndication/#sampleFeed |
|
1623 | ||
1624 |
s_example = """\ |
|
1625 |
<?xml version="1.0" encoding="utf-8"?> |
|
1626 |
<feed xmlns="http://www.w3.org/2005/Atom"> |
|
1627 |
<title>Example Feed</title> |
|
1628 |
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> |
|
1629 |
<updated>2003-12-13T18:30:02Z</updated> |
|
1630 |
<author> |
|
1631 |
<name>John Doe</name> |
|
1632 |
</author> |
|
1633 |
<link href="http://example.org/"/> |
|
1634 |
<entry> |
|
1635 |
<title>Atom-Powered Robots Run Amok</title> |
|
1636 |
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> |
|
1637 |
<updated>2003-12-13T18:30:02Z</updated> |
|
1638 |
<link href="http://example.org/2003/12/13/atom03"/> |
|
1639 |
<summary>Some text.</summary> |
|
1640 |
</entry> |
|
1641 |
</feed>""" |
|
1642 | ||
1643 |
xmldoc = XMLDoc() |
|
1644 |
|
|
1645 |
feed = Feed() |
|
1646 |
xmldoc.root_element = feed |
|
1647 | ||
1648 |
feed.title = "Example Feed" |
|
1649 |
feed.id = "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6" |
|
1650 |
feed.updated = "2003-12-13T18:30:02Z" |
|
1651 | ||
1652 |
link = Link("http://example.org/") |
|
1653 |
feed.links.append(link) |
|
1654 | ||
1655 |
author = Author("John Doe") |
|
1656 |
feed.authors.append(author) |
|
1657 | ||
1658 | ||
1659 |
entry = Entry() |
|
1660 |
feed.entries.append(entry) |
|
1661 |
entry.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a" |
|
1662 |
entry.title = "Atom-Powered Robots Run Amok" |
|
1663 |
entry.updated = "2003-12-13T18:30:02Z" |
|
1664 |
entry.summary = "Some text." |
|
1665 | ||
1666 |
link = Link("http://example.org/2003/12/13/atom03") |
|
1667 |
entry.links.append(link) |
|
1668 | ||
1669 | ||
1670 |
s = str(xmldoc) |
|
1671 |
if s_example != s: |
|
1672 |
failed_tests += 1 |
|
1673 |
print "test case failed:" |
|
1674 |
print "The generated XML doesn't match the example. diff follows:" |
|
1675 |
print diff(s_example, "s_example", s, "s") |
|
1676 | ||
1677 | ||
1678 |
# Test: verify that xmldoc.Validate() succeeds |
|
1679 | ||
1680 |
if not xmldoc.Validate(): |
|
1681 |
failed_tests += 1 |
|
1682 |
print "test case failed:" |
|
1683 |
print "xmldoc.Validate() failed." |
|
1684 | ||
1685 | ||
1686 |
# Test: does Element work both nested an non-nested? |
|
1687 |
s_test = """\ |
|
1688 |
<test> |
|
1689 |
<test:agent number="007">James Bond</test:agent> |
|
1690 |
<test:pet |
|
1691 |
nickname="Mei-Mei" |
|
1692 |
type="cat">Matrix</test:pet> |
|
1693 |
</test>""" |
|
1694 | ||
1695 |
class TestPet(Element): |
|
1696 |
def __init__(self, name=""): |
|
1697 |
Element.__init__(self, "test:pet", None, None) |
|
1698 |
self.text = name |
|
1699 | ||
1700 |
class TestAgent(Element): |
|
1701 |
def __init__(self, name=""): |
|
1702 |
Element.__init__(self, "test:agent", None, None) |
|
1703 |
self.text = name |
|
1704 | ||
1705 |
class Test(Element): |
|
1706 |
def __init__(self): |
|
1707 |
Element.__init__(self, "test", None, None) |
|
1708 |
self.test_agent = TestAgent() |
|
1709 |
self.test_pet = TestPet() |
|
1710 | ||
1711 |
test = Test() |
|
1712 |
test.test_agent = "James Bond" |
|
1713 |
test.test_agent.attrs["number"] = "007" |
|
1714 |
test.test_pet = "Matrix" |
|
1715 |
test.test_pet.attrs["type"] = "cat" |
|
1716 |
test.test_pet.attrs["nickname"] = "Mei-Mei" |
|
1717 | ||
1718 |
s = str(test) |
|
1719 |
if s_test != s: |
|
1720 |
failed_tests += 1 |
|
1721 |
print "test case failed:" |
|
1722 |
print "test output doesn't match. diff follows:" |
|
1723 |
print diff(s_test, "s_test", s, "s") |
|
1724 | ||
1725 | ||
1726 |
if failed_tests > 0: |
|
1727 |
print "self-test failed!" |
|
1728 |
else: |
|
1729 |
print "self-test successful." |
|
1730 | ||
1731 | ||
1732 | ||
1733 |
if __name__ == "__main__": |
|
1734 |
run_test_cases() |
auquotidien/modules/pyatom/readme.txt | ||
---|---|---|
1 |
PyAtom |
|
2 | ||
3 | ||
4 |
PyAtom is a Python library module I wrote to make it very easy to create |
|
5 |
an Atom syndication feed. |
|
6 | ||
7 |
http://atomenabled.org/developers/syndication/ |
|
8 | ||
9 | ||
10 |
I have released PyAtom under The Academic Free License 2.1. I intend to |
|
11 |
donate PyAtom to the Python Software Foundation. |
|
12 | ||
13 | ||
14 |
Notes on PyAtom: |
|
15 | ||
16 |
XML is best represented in a tree structure, and PyAtom is a set of |
|
17 |
classes that automatically manage the tree structure for the user. The |
|
18 |
top level of an Atom feed is an XML "Feed" element with a "<feed>" tag; |
|
19 |
the Feed element has other elements nested inside it that describe the |
|
20 |
feed, and then it has 0 or more Entry elements, each of which has |
|
21 |
elements that describe the Entry. |
|
22 | ||
23 |
Take a look at RunPyAtomTestCases(), at the end of pyatom.py, for |
|
24 |
example code showing how to set up a feed with an entry. |
|
25 | ||
26 |
To create an XML document with a feed in it, the user does this: |
|
27 | ||
28 |
xmldoc = XMLDoc() |
|
29 |
feed = Feed() |
|
30 |
xmldoc.root_element = feed |
|
31 | ||
32 |
To assign an entry to a feed, the user just does this: |
|
33 | ||
34 |
feed.entries.append(entry) |
|
35 | ||
36 |
This adds "entry" to the internal list that keeps track of entries. |
|
37 |
"entry" is now nested inside "feed", which is nested inside "xmldoc". |
|
38 | ||
39 |
Later, when the user wants to save the XML in a file, the user can just |
|
40 |
do this: |
|
41 | ||
42 |
f = open("file.xml", "w") |
|
43 |
s = str(xmldoc) |
|
44 |
f.write(s) |
|
45 | ||
46 |
To make the string from xmldoc, the XMLDoc class walks through the XML |
|
47 |
elements nested inside xmldoc, asking each one to return its string. |
|
48 |
Each element that has other elements nested inside does the same thing. |
|
49 |
The whole tree is recursively walked, and the tags all return strings |
|
50 |
that are indented properly for their level in the tree. |
|
51 | ||
52 |
The classes that implement Atom in PyAtom just use the heck out of |
|
53 |
inheritance. There are abstract base classes that implement broadly |
|
54 |
useful behavior, and lots of classes that just inherit and use this |
|
55 |
behavior; but there are plenty of places where the child classes |
|
56 |
overload the inherited behavior and do something different. The way |
|
57 |
Python handles inheritance made this a joy to code up. |
|
58 | ||
59 | ||
60 | ||
61 |
If you have any questions about anything here, please contact me using |
|
62 |
this email address: |
|
63 | ||
64 |
pyatom@langri.com |
auquotidien/modules/root.py | ||
---|---|---|
34 | 34 |
from wcs.qommon.admin.emails import EmailsDirectory |
35 | 35 |
from wcs.qommon.admin.texts import TextsDirectory |
36 | 36 | |
37 |
from .announces import Announce, AnnounceSubscription |
|
38 | 37 |
from .myspace import MyspaceDirectory |
39 | 38 |
from .payments import PublicPaymentDirectory |
40 | 39 |
from .payments_ui import InvoicesDirectory |
... | ... | |
134 | 133 |
return r.getvalue() |
135 | 134 | |
136 | 135 | |
137 |
class AnnounceDirectory(Directory): |
|
138 |
_q_exports = ['', 'edit', 'delete', 'email'] |
|
139 | ||
140 |
def __init__(self, announce): |
|
141 |
self.announce = announce |
|
142 | ||
143 |
def _q_index(self): |
|
144 |
template.html_top(_('Announces to citizens')) |
|
145 |
r = TemplateIO(html=True) |
|
146 | ||
147 |
if self.announce.publication_time: |
|
148 |
date_heading = '%s - ' % time.strftime(misc.date_format(), self.announce.publication_time) |
|
149 |
else: |
|
150 |
date_heading = '' |
|
151 | ||
152 |
r += htmltext('<h3>%s%s</h3>') % (date_heading, self.announce.title) |
|
153 | ||
154 |
r += htmltext('<p>') |
|
155 |
r += self.announce.text |
|
156 |
r += htmltext('</p>') |
|
157 | ||
158 |
r += htmltext('<p>') |
|
159 |
r += htmltext('<a href="../">%s</a>') % _('Back') |
|
160 |
r += htmltext('</p>') |
|
161 |
return r.getvalue() |
|
162 | ||
163 | ||
164 |
class AnnouncesDirectory(Directory): |
|
165 |
_q_exports = ['', 'subscribe', 'email', 'atom', 'sms', 'emailconfirm', |
|
166 |
'email_unsubscribe', 'sms_unsubscribe', 'smsconfirm', 'rawlist'] |
|
167 | ||
168 |
def _q_traverse(self, path): |
|
169 |
get_response().breadcrumb.append(('announces/', _('Announces'))) |
|
170 |
return Directory._q_traverse(self, path) |
|
171 | ||
172 |
def _q_index(self): |
|
173 |
template.html_top(_('Announces to citizens')) |
|
174 |
r = TemplateIO(html=True) |
|
175 |
r += self.announces_list() |
|
176 |
r += htmltext('<ul id="announces-links">') |
|
177 |
r += htmltext('<li><a href="subscribe">%s</a></li>') % _('Receiving those Announces') |
|
178 |
r += htmltext('</ul>') |
|
179 |
return r.getvalue() |
|
180 | ||
181 |
def _get_announce_subscription(self): |
|
182 |
""" """ |
|
183 |
sub = None |
|
184 |
if get_request().user: |
|
185 |
subs = AnnounceSubscription.select(lambda x: x.user_id == get_request().user.id) |
|
186 |
if subs: |
|
187 |
sub = subs[0] |
|
188 |
return sub |
|
189 | ||
190 |
def rawlist(self): |
|
191 |
get_response().filter = None |
|
192 |
return self.announces_list() |
|
193 | ||
194 |
def announces_list(self): |
|
195 |
announces = Announce.get_published_announces() |
|
196 |
if not announces: |
|
197 |
raise errors.TraversalError() |
|
198 | ||
199 |
# XXX: will need pagination someday |
|
200 |
r = TemplateIO(html=True) |
|
201 |
for item in announces: |
|
202 |
r += htmltext('<div class="announce-item">\n') |
|
203 |
r += htmltext('<h4>') |
|
204 |
if item.publication_time: |
|
205 |
r += time.strftime(misc.date_format(), item.publication_time) |
|
206 |
r += ' - ' |
|
207 |
r += item.title |
|
208 |
r += htmltext('</h4>\n') |
|
209 |
r += htmltext('<p>\n') |
|
210 |
r += item.text |
|
211 |
r += htmltext('\n</p>\n') |
|
212 |
r += htmltext('</div>\n') |
|
213 |
return r.getvalue() |
|
214 | ||
215 | ||
216 |
def sms(self): |
|
217 |
sms_mode = get_cfg('sms', {}).get('mode', 'none') |
|
218 | ||
219 |
if sms_mode == 'none': |
|
220 |
raise errors.TraversalError() |
|
221 | ||
222 |
get_response().breadcrumb.append(('sms', _('SMS'))) |
|
223 |
template.html_top(_('Receiving announces by SMS')) |
|
224 |
r = TemplateIO(html=True) |
|
225 | ||
226 |
if sms_mode == 'demo': |
|
227 |
r += TextsDirectory.get_html_text('aq-sms-demo') |
|
228 |
else: |
|
229 |
announces_cfg = get_cfg('announces',{}) |
|
230 |
mobile_mask = announces_cfg.get('mobile_mask') |
|
231 |
if mobile_mask: |
|
232 |
mobile_mask = ' (' + mobile_mask + ')' |
|
233 |
else: |
|
234 |
mobile_mask = '' |
|
235 |
form = Form(enctype='multipart/form-data') |
|
236 |
form.add(StringWidget, 'mobile', title = _('Mobile number %s') % mobile_mask, size=12, required=True) |
|
237 |
form.add_submit('submit', _('Subscribe')) |
|
238 |
form.add_submit('cancel', _('Cancel')) |
|
239 | ||
240 |
if form.get_submit() == 'cancel': |
|
241 |
return redirect('subscribe') |
|
242 | ||
243 |
if form.is_submitted() and not form.has_errors(): |
|
244 |
s = self.sms_submit(form) |
|
245 |
if s == False: |
|
246 |
r += form.render() |
|
247 |
else: |
|
248 |
return redirect("smsconfirm") |
|
249 |
else: |
|
250 |
r += form.render() |
|
251 |
return r.getvalue() |
|
252 | ||
253 |
def sms_submit(self, form): |
|
254 |
mobile = form.get_widget("mobile").parse() |
|
255 |
# clean the string, remove any extra character |
|
256 |
mobile = re.sub('[^0-9+]','',mobile) |
|
257 |
# if a mask was set, validate |
|
258 |
announces_cfg = get_cfg('announces',{}) |
|
259 |
mobile_mask = announces_cfg.get('mobile_mask') |
|
260 |
if mobile_mask: |
|
261 |
mobile_regexp = re.sub('X','[0-9]', mobile_mask) + '$' |
|
262 |
if not re.match(mobile_regexp, mobile): |
|
263 |
form.set_error("mobile", _("Phone number invalid ! It must match ") + mobile_mask) |
|
264 |
return False |
|
265 |
if mobile.startswith('00'): |
|
266 |
mobile = '+' + mobile[2:] |
|
267 |
else: |
|
268 |
# Default to france international prefix |
|
269 |
if not mobile.startswith('+'): |
|
270 |
mobile = re.sub("^0", "+33", mobile) |
|
271 |
sub = self._get_announce_subscription() |
|
272 |
if not sub: |
|
273 |
sub = AnnounceSubscription() |
|
274 |
if get_request().user: |
|
275 |
sub.user_id = get_request().user.id |
|
276 | ||
277 |
if mobile: |
|
278 |
sub.sms = mobile |
|
279 | ||
280 |
if not get_request().user: |
|
281 |
sub.enabled = False |
|
282 | ||
283 |
sub.store() |
|
284 | ||
285 |
# Asking sms confirmation |
|
286 |
token = Token(3 * 86400, 4, string.digits) |
|
287 |
token.type = 'announces-subscription-confirmation' |
|
288 |
token.subscription_id = sub.id |
|
289 |
token.store() |
|
290 | ||
291 |
message = _("Confirmation code : %s") % str(token.id) |
|
292 |
sms_cfg = get_cfg('sms', {}) |
|
293 |
sender = sms_cfg.get('sender', 'AuQuotidien')[:11] |
|
294 |
mode = sms_cfg.get('mode', 'none') |
|
295 |
sms = SMS.get_sms_class(mode) |
|
296 |
try: |
|
297 |
sms.send(sender, [mobile], message) |
|
298 |
except errors.SMSError, e: |
|
299 |
get_logger().error(e) |
|
300 |
form.set_error("mobile", _("Send SMS confirmation failed")) |
|
301 |
sub.remove("sms") |
|
302 |
return False |
|
303 | ||
304 |
def smsconfirm(self): |
|
305 |
template.html_top(_('Receiving announces by SMS confirmation')) |
|
306 |
r = TemplateIO(html=True) |
|
307 |
r += htmltext("<p>%s</p>") % _("You will receive a confirmation code by SMS.") |
|
308 |
form = Form(enctype='multipart/form-data') |
|
309 |
form.add(StringWidget, 'code', title = _('Confirmation code (4 characters)'), size=12, required=True) |
|
310 |
form.add_submit('submit', _('Subscribe')) |
|
311 |
form.add_submit('cancel', _('Cancel')) |
|
312 | ||
313 |
if form.get_submit() == 'cancel': |
|
314 |
return redirect('..') |
|
315 | ||
316 |
if form.is_submitted() and not form.has_errors(): |
|
317 |
token = None |
|
318 |
id = form.get_widget("code").parse() |
|
319 |
try: |
|
320 |
token = Token.get(id) |
|
321 |
except KeyError: |
|
322 |
form.set_error("code", _('Invalid confirmation code.')) |
|
323 |
else: |
|
324 |
if token.type != 'announces-subscription-confirmation': |
|
325 |
form.set_error("code", _('Invalid confirmation code.')) |
|
326 |
else: |
|
327 |
sub = AnnounceSubscription.get(token.subscription_id) |
|
328 |
token.remove_self() |
|
329 |
sub.enabled_sms = True |
|
330 |
sub.store() |
|
331 |
return redirect('.') |
|
332 |
r += form.render() |
|
333 |
else: |
|
334 |
r += form.render() |
|
335 | ||
336 |
return r.getvalue() |
|
337 | ||
338 |
def sms_unsubscribe(self): |
|
339 |
sub = self._get_announce_subscription() |
|
340 | ||
341 |
form = Form(enctype='multipart/form-data') |
|
342 |
if not sub: |
|
343 |
return redirect('..') |
|
344 | ||
345 |
form.add_submit('submit', _('Unsubscribe')) |
|
346 |
form.add_submit('cancel', _('Cancel')) |
|
347 | ||
348 |
if form.get_submit() == 'cancel': |
|
349 |
return redirect('..') |
|
350 | ||
351 |
get_response().breadcrumb.append(('sms', _('SMS Unsubscription'))) |
|
352 |
template.html_top() |
|
353 |
r = TemplateIO(html=True) |
|
354 | ||
355 |
if form.is_submitted() and not form.has_errors(): |
|
356 |
if sub: |
|
357 |
sub.remove("sms") |
|
358 | ||
359 |
root_url = get_publisher().get_root_url() |
|
360 |
r += htmltext('<p>') |
|
361 |
r += _('You have been unsubscribed from announces') |
|
362 |
r += htmltext('</p>') |
|
363 |
r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home')) |
|
364 |
else: |
|
365 |
r += htmltext('<p>') |
|
366 |
r += _('Do you want to stop receiving announces by sms ?') |
|
367 |
r += htmltext('</p>') |
|
368 |
r += form.render() |
|
369 | ||
370 |
return r.getvalue() |
|
371 | ||
372 | ||
373 |
def subscribe(self): |
|
374 |
get_response().breadcrumb.append(('subscribe', _('Subscription'))) |
|
375 |
template.html_top(_('Receiving Announces')) |
|
376 |
r = TemplateIO(html=True) |
|
377 | ||
378 |
r += TextsDirectory.get_html_text('aq-announces-subscription') |
|
379 | ||
380 |
sub = self._get_announce_subscription() |
|
381 | ||
382 |
r += htmltext('<ul id="announce-modes">') |
|
383 |
if sub and sub.email: |
|
384 |
r += htmltext(' <li>') |
|
385 |
r += htmltext('<span id="par_mail">%s</span>') % _('Email (currently subscribed)') |
|
386 |
r += htmltext(' <a href="email_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe') |
|
387 |
else: |
|
388 |
r += htmltext(' <li><a href="email" id="par_mail" rel="popup">%s</a></li>') % _('Email') |
|
389 |
if sub and sub.sms: |
|
390 |
r += htmltext(' <li>') |
|
391 |
if sub.enabled_sms: |
|
392 |
r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently subscribed)') % sub.sms |
|
393 |
else: |
|
394 |
r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently not confirmed)') % sub.sms |
|
395 |
r += htmltext(' <a href="smsconfirm" rel="popup">%s</a> ') % _('Confirmation') |
|
396 |
r += htmltext(' <a href="sms_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe') |
|
397 |
elif get_cfg('sms', {}).get('mode', 'none') != 'none': |
|
398 |
r += htmltext(' <li><a href="sms" id="par_sms">%s</a></li>') % _('SMS') |
|
399 |
r += htmltext(' <li><a class="feed-link" href="atom" id="par_rss">%s</a>') % _('Feed') |
|
400 |
r += htmltext('</ul>') |
|
401 |
return r.getvalue() |
|
402 | ||
403 |
def email(self): |
|
404 |
get_response().breadcrumb.append(('email', _('Email Subscription'))) |
|
405 |
template.html_top(_('Receiving Announces by email')) |
|
406 |
r = TemplateIO(html=True) |
|
407 | ||
408 |
form = Form(enctype='multipart/form-data') |
|
409 |
if get_request().user: |
|
410 |
if get_request().user.email: |
|
411 |
r += htmltext('<p>') |
|
412 |
r += _('You are logged in and your email is %s, ok to subscribe ?') % \ |
|
413 |
get_request().user.email |
|
414 |
r += htmltext('</p>') |
|
415 |
form.add_submit('submit', _('Subscribe')) |
|
416 |
else: |
|
417 |
r += htmltext('<p>') |
|
418 |
r += _("You are logged in but there is no email address in your profile.") |
|
419 |
r += htmltext('</p>') |
|
420 |
form.add(EmailWidget, 'email', title = _('Email'), required = True) |
|
421 |
form.add_submit('submit', _('Subscribe')) |
|
422 |
form.add_submit('submit-remember', _('Subscribe and add this email to my profile')) |
|
423 |
else: |
|
424 |
r += htmltext('<p>') |
|
425 |
r += _('FIXME will only be used for this purpose etc.') |
|
426 |
r += htmltext('</p>') |
|
427 |
form.add(EmailWidget, 'email', title = _('Email'), required = True) |
|
428 |
form.add_submit('submit', _('Subscribe')) |
|
429 | ||
430 |
form.add_submit('cancel', _('Cancel')) |
|
431 | ||
432 |
if form.get_submit() == 'cancel': |
|
433 |
return redirect('subscribe') |
|
434 | ||
435 |
if form.is_submitted() and not form.has_errors(): |
|
436 |
s = self.email_submit(form) |
|
437 |
if s is not False: |
|
438 |
return s |
|
439 |
else: |
|
440 |
r += form.render() |
|
441 | ||
442 |
return r.getvalue() |
|
443 | ||
444 |
def email_submit(self, form): |
|
445 |
sub = self._get_announce_subscription() |
|
446 |
if not sub: |
|
447 |
sub = AnnounceSubscription() |
|
448 | ||
449 |
if get_request().user: |
|
450 |
sub.user_id = get_request().user.id |
|
451 | ||
452 |
if form.get_widget('email'): |
|
453 |
sub.email = form.get_widget('email').parse() |
|
454 |
elif get_request().user.email: |
|
455 |
sub.email = get_request().user.email |
|
456 | ||
457 |
if not get_request().user: |
|
458 |
sub.enabled = False |
|
459 | ||
460 |
sub.store() |
|
461 | ||
462 |
if get_request().user: |
|
463 |
r = TemplateIO(html=True) |
|
464 |
root_url = get_publisher().get_root_url() |
|
465 |
r += htmltext('<p>') |
|
466 |
r += _('You have been subscribed to the announces.') |
|
467 |
r += htmltext('</p>') |
|
468 |
r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home')) |
|
469 |
return r.getvalue() |
|
470 | ||
471 |
# asking email confirmation before subscribing someone |
|
472 |
token = Token(3 * 86400) |
|
473 |
token.type = 'announces-subscription-confirmation' |
|
474 |
token.subscription_id = sub.id |
|
475 |
token.store() |
|
476 |
data = { |
|
477 |
'confirm_url': get_request().get_url() + 'confirm?t=%s&a=cfm' % token.id, |
|
478 |
'cancel_url': get_request().get_url() + 'confirm?t=%s&a=cxl' % token.id, |
|
479 |
'time': misc.localstrftime(time.localtime(token.expiration)), |
|
480 |
} |
|
481 | ||
482 |
emails.custom_template_email('announces-subscription-confirmation', |
|
483 |
data, sub.email, exclude_current_user = False) |
|
484 | ||
485 |
r = TemplateIO(html=True) |
|
486 |
root_url = get_publisher().get_root_url() |
|
487 |
r += htmltext('<p>') |
|
488 |
r += _('You have been sent an email for confirmation') |
|
489 |
r += htmltext('</p>') |
|
490 |
r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home')) |
|
491 |
return r.getvalue() |
|
492 | ||
493 |
def emailconfirm(self): |
|
494 |
tokenv = get_request().form.get('t') |
|
495 |
action = get_request().form.get('a') |
|
496 | ||
497 |
root_url = get_publisher().get_root_url() |
|
498 | ||
499 |
try: |
|
500 |
token = Token.get(tokenv) |
|
501 |
except KeyError: |
|
502 |
return template.error_page( |
|
503 |
_('The token you submitted does not exist, has expired, or has been cancelled.'), |
|
504 |
continue_to = (root_url, _('home page'))) |
|
505 | ||
506 |
if token.type != 'announces-subscription-confirmation': |
|
507 |
return template.error_page( |
|
508 |
_('The token you submitted is not appropriate for the requested task.'), |
|
509 |
continue_to = (root_url, _('home page'))) |
|
510 | ||
511 |
sub = AnnounceSubscription.get(token.subscription_id) |
|
512 | ||
513 |
if action == 'cxl': |
|
514 |
r = TemplateIO(html=True) |
|
515 |
root_url = get_publisher().get_root_url() |
|
516 |
template.html_top(_('Email Subscription')) |
|
517 |
r += htmltext('<h1>%s</h1>') % _('Request Cancelled') |
|
518 |
r += htmltext('<p>%s</p>') % _('The request for subscription has been cancelled.') |
|
519 |
r += htmltext('<p>') |
|
520 |
r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url) |
|
521 |
r += htmltext('</p>') |
|
522 |
token.remove_self() |
|
523 |
sub.remove_self() |
|
524 |
return r.getvalue() |
|
525 | ||
526 |
if action == 'cfm': |
|
527 |
token.remove_self() |
|
528 |
sub.enabled = True |
|
529 |
sub.store() |
|
530 |
r = TemplateIO(html=True) |
|
531 |
root_url = get_publisher().get_root_url() |
|
532 |
template.html_top(_('Email Subscription')) |
|
533 |
r += htmltext('<h1>%s</h1>') % _('Subscription Confirmation') |
|
534 |
r += htmltext('<p>%s</p>') % _('Your subscription to announces is now effective.') |
|
535 |
r += htmltext('<p>') |
|
536 |
r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url) |
|
537 |
r += htmltext('</p>') |
|
538 |
return r.getvalue() |
|
539 | ||
540 |
def atom(self): |
|
541 |
response = get_response() |
|
542 |
response.set_content_type('application/atom+xml') |
|
543 | ||
544 |
from pyatom import pyatom |
|
545 |
xmldoc = pyatom.XMLDoc() |
|
546 |
feed = pyatom.Feed() |
|
547 |
xmldoc.root_element = feed |
|
548 |
feed.title = get_cfg('misc', {}).get('sitename') or 'Publik' |
|
549 |
feed.id = get_request().get_url() |
|
550 | ||
551 |
author_email = get_cfg('emails', {}).get('reply_to') |
|
552 |
if not author_email: |
|
553 |
author_email = get_cfg('emails', {}).get('from') |
|
554 |
if author_email: |
|
555 |
feed.authors.append(pyatom.Author(author_email)) |
|
556 | ||
557 |
announces = Announce.get_published_announces() |
|
558 | ||
559 |
if announces and announces[0].modification_time: |
|
560 |
feed.updated = misc.format_time(announces[0].modification_time, |
|
561 |
'%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ', |
|
562 |
gmtime = True) |
|
563 |
feed.links.append(pyatom.Link(get_request().get_url(1) + '/')) |
|
564 | ||
565 |
for item in announces: |
|
566 |
entry = item.get_atom_entry() |
|
567 |
if entry: |
|
568 |
feed.entries.append(entry) |
|
569 | ||
570 |
return str(feed) |
|
571 | ||
572 |
def email_unsubscribe(self): |
|
573 |
sub = self._get_announce_subscription() |
|
574 | ||
575 |
form = Form(enctype='multipart/form-data') |
|
576 |
if not sub: |
|
577 |
form.add(EmailWidget, 'email', title = _('Email'), required = True) |
|
578 | ||
579 |
form.add_submit('submit', _('Unsubscribe')) |
|
580 |
form.add_submit('cancel', _('Cancel')) |
|
581 | ||
582 |
if form.get_submit() == 'cancel': |
|
583 |
return redirect('..') |
|
584 | ||
585 |
get_response().breadcrumb.append(('email', _('Email Unsubscription'))) |
|
586 |
template.html_top() |
|
587 |
r = TemplateIO(html=True) |
|
588 | ||
589 |
if form.is_submitted() and not form.has_errors(): |
|
590 |
if sub: |
|
591 |
sub.remove("email") |
|
592 |
else: |
|
593 |
email = form.get_widget('email').parse() |
|
594 |
for s in AnnounceSubscription.select(): |
|
595 |
if s.email == email: |
|
596 |
s.remove("email") |
|
597 | ||
598 |
root_url = get_publisher().get_root_url() |
|
599 |
r += htmltext('<p>') |
|
600 |
r += _('You have been unsubscribed from announces') |
|
601 |
r += htmltext('</p>') |
|
602 |
r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home')) |
|
603 | ||
604 |
else: |
|
605 |
r += htmltext('<p>') |
|
606 |
r += _('Do you want to stop receiving announces by email?') |
|
607 |
r += htmltext('</p>') |
|
608 |
r += form.render() |
|
609 | ||
610 |
return r.getvalue() |
|
611 | ||
612 |
def _q_lookup(self, component): |
|
613 |
try: |
|
614 |
announce = Announce.get(component) |
|
615 |
except KeyError: |
|
616 |
raise errors.TraversalError() |
|
617 | ||
618 |
if announce.hidden: |
|
619 |
raise errors.TraversalError() |
|
620 | ||
621 |
get_response().breadcrumb.append((str(announce.id), announce.title)) |
|
622 |
return AnnounceDirectory(announce) |
|
623 | ||
624 | 136 |
OldRegisterDirectory = wcs.root.RegisterDirectory |
625 | 137 | |
626 | 138 |
class AlternateRegisterDirectory(OldRegisterDirectory): |
... | ... | |
757 | 269 |
_q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout', |
758 | 270 |
'saml', 'register', 'ident', 'afterjobs', |
759 | 271 |
('informations-editeur', 'informations_editeur'), |
760 |
('announces', 'announces_dir'), |
|
761 | 272 |
'myspace', 'services', 'categories', 'user', |
762 | 273 |
('tmp-upload', 'tmp_upload'), 'json', '__version__', |
763 | 274 |
'themes', 'pages', 'payment', 'invoices', 'roles', |
... | ... | |
765 | 276 |
('reload-top', 'reload_top'), 'static', |
766 | 277 |
('i18n.js', 'i18n_js'), 'actions',] |
767 | 278 | |
768 |
announces_dir = AnnouncesDirectory() |
|
769 | 279 |
register = AlternateRegisterDirectory() |
770 | 280 |
login = AlternateLoginDirectory() |
771 | 281 |
ident = AlternateIdentDirectory() |
... | ... | |
909 | 419 |
r += self.myspace_snippet() |
910 | 420 |
r += self.box_services(position='2nd') |
911 | 421 |
r += self.consultations() |
912 |
r += self.announces() |
|
913 | 422 |
r += htmltext('</div>') |
914 | 423 | |
915 | 424 |
user = get_request().user |
... | ... | |
1105 | 614 |
def has_anonymous_access_codes(self): |
1106 | 615 |
return any((x for x in FormDef.select() if x.enable_tracking_codes)) |
1107 | 616 | |
1108 |
def announces(self): |
|
1109 |
announces = Announce.get_published_announces() |
|
1110 |
if not announces: |
|
1111 |
return |
|
1112 | ||
1113 |
r = TemplateIO(html=True) |
|
1114 |
r += htmltext('<div id="announces">') |
|
1115 |
r += htmltext('<h3>%s</h3>') % _('Announces to citizens') |
|
1116 |
for item in announces[:3]: |
|
1117 |
r += htmltext('<div class="announce-item">') |
|
1118 |
r += htmltext('<h4>') |
|
1119 |
if item.publication_time: |
|
1120 |
r += time.strftime(misc.date_format(), item.publication_time) |
|
1121 |
r += ' - ' |
|
1122 |
r += item.title |
|
1123 |
r += htmltext('</h4>') |
|
1124 |
r += htmltext('<p>') |
|
1125 |
r += item.text |
|
1126 |
r += htmltext('</p>') |
|
1127 |
r += htmltext('</div>') |
|
1128 | ||
1129 |
r += htmltext('<ul id="announces-links">') |
|
1130 |
r += htmltext('<li><a href="announces/subscribe">%s</a></li>') % _('Receiving those Announces') |
|
1131 |
r += htmltext('<li><a href="announces/">%s</a></li>') % _('Previous Announces') |
|
1132 |
r += htmltext('</ul>') |
|
1133 |
r += htmltext('</div>') |
|
1134 |
return r.getvalue() |
|
1135 | ||
1136 | 617 |
def myspace_snippet(self): |
1137 | 618 |
r = TemplateIO(html=True) |
1138 | 619 |
r += htmltext('<div id="myspace">') |
... | ... | |
1183 | 664 |
get_publisher_class().use_sms_feature = True |
1184 | 665 | |
1185 | 666 | |
1186 |
EmailsDirectory.register('announces-subscription-confirmation', |
|
1187 |
N_('Confirmation of Announces Subscription'), |
|
1188 |
N_('Available variables: change_url, cancel_url, time, sitename'), |
|
1189 |
default_subject = N_('Announce Subscription Request'), |
|
1190 |
default_body = N_("""\ |
|
1191 |
You have (or someone impersonating you has) requested to subscribe to |
|
1192 |
announces from [sitename]. To confirm this request, visit the |
|
1193 |
following link: |
|
1194 | ||
1195 |
[confirm_url] |
|
1196 | ||
1197 |
If you are not the person who made this request, or you wish to cancel |
|
1198 |
this request, visit the following link: |
|
1199 | ||
1200 |
[cancel_url] |
|
1201 | ||
1202 |
If you do nothing, the request will lapse after 3 days (precisely on |
|
1203 |
[time]). |
|
1204 |
""")) |
|
1205 | ||
1206 | ||
1207 |
TextsDirectory.register('aq-announces-subscription', |
|
1208 |
N_('Text on announces subscription page'), |
|
1209 |
default = N_('''\ |
|
1210 |
<p> |
|
1211 |
FIXME |
|
1212 |
'</p>''')) |
|
1213 | ||
1214 |
TextsDirectory.register('aq-sms-demo', |
|
1215 |
N_('Text when subscribing to announces SMS and configured as demo'), |
|
1216 |
default = N_(''' |
|
1217 |
<p> |
|
1218 |
Receiving announces by SMS is not possible in this demo |
|
1219 |
</p>''')) |
|
1220 | ||
1221 | 667 |
TextsDirectory.register('aq-editor-info', N_('Editor Informations')) |
1222 | 668 |
TextsDirectory.register('aq-accessibility', N_('Accessibility Statement')) |
1223 | 669 |
TextsDirectory.register('aq-contact', N_('Contact Information')) |
auquotidien/modules/template.py | ||
---|---|---|
43 | 43 |
elif section == 'consultations': |
44 | 44 |
section_title = '<h2 id="consultations">%s</h2>\n' % _('Consultations') |
45 | 45 |
response.filter['bigdiv'] = 'rub_consultation' |
46 |
elif section == 'announces': |
|
47 |
response.filter['bigdiv'] = 'rub_annonce' |
|
48 |
section_title = '<h2 id="announces">%s</h2>\n' % _('Announces to citizens') |
|
49 |
if page_title == _('Announces to citizens'): |
|
50 |
page_title = '' |
|
51 | 46 |
elif section and len(section) > 1: |
52 | 47 |
# XXX: this works but is not efficient |
53 | 48 |
if Category.has_urlname(section): |
54 |
- |