Projet

Général

Profil

« Précédent | Suivant » 

Révision 741fa074

Ajouté par Frédéric Péters il y a plus de 4 ans

remove support for announces (#37967)

Voir les différences:

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'&nbsp;')
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 ("&nbsp;").
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
	&lt;head&gt;
191
	>>> print s_escape_html("&nbsp;")
192
	&amp;nbsp;
193
	"""
194
	s = s.replace("&", "&amp;")
195
	s = s.replace("<", "&lt;")
196
	s = s.replace(">", "&gt;")
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):

Formats disponibles : Unified diff