Merge lp:~jskrzeszewska/mailman/mailman into lp:mailman

Proposed by Joanna Skrzeszewska on 2013-08-03
Status: Needs review
Proposed branch: lp:~jskrzeszewska/mailman/mailman
Merge into: lp:mailman
Diff against target: 553 lines (+513/-0)
5 files modified
src/mailman/archiving/rssarchive.py (+225/-0)
src/mailman/archiving/tests/test_rssarchive.py (+203/-0)
src/mailman/config/mailman.cfg (+4/-0)
src/mailman/rest/root.py (+9/-0)
src/mailman/rest/rss.py (+72/-0)
To merge this branch: bzr merge lp:~jskrzeszewska/mailman/mailman
Reviewer Review Type Date Requested Status
Nicki Hutchens (community) Approve on 2013-09-24
Nicki Hutchens 2013-08-14 Pending
Barry Warsaw 2013-08-03 Pending
Review via email: mp+178414@code.launchpad.net

Description of the change

Added archiver for rss feed and REST API for rss settings.

To post a comment you must log in.
lp:~jskrzeszewska/mailman/mailman updated on 2013-09-01
7218. By Joanna Skrzeszewska on 2013-09-01

Changes in rssarchive.py

7219. By Joanna Skrzeszewska on 2013-09-01

Tests for rssarchive

Nicki Hutchens (nhutch01) :
review: Approve

Unmerged revisions

7219. By Joanna Skrzeszewska on 2013-09-01

Tests for rssarchive

7218. By Joanna Skrzeszewska on 2013-09-01

Changes in rssarchive.py

7217. By Joanna Skrzeszewska on 2013-08-02

Adding REST API for rss archiver.

7216. By Joanna Skrzeszewska on 2013-08-02

Adding rss archiver.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/mailman/archiving/rssarchive.py'
2--- src/mailman/archiving/rssarchive.py 1970-01-01 00:00:00 +0000
3+++ src/mailman/archiving/rssarchive.py 2013-09-01 16:37:50 +0000
4@@ -0,0 +1,225 @@
5+# Copyright (C) 2008-2013 by the Free Software Foundation, Inc.
6+#
7+# This file is part of GNU Mailman.
8+#
9+# GNU Mailman is free software: you can redistribute it and/or modify it under
10+# the terms of the GNU General Public License as published by the Free
11+# Software Foundation, either version 3 of the License, or (at your option)
12+# any later version.
13+#
14+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
15+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17+# more details.
18+#
19+# You should have received a copy of the GNU General Public License along with
20+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
21+
22+from __future__ import absolute_import, print_function, unicode_literals
23+
24+__metaclass__ = type
25+__all__ = [
26+ 'RssArchiver',
27+ ]
28+
29+import os
30+import errno
31+import logging
32+import datetime
33+import PyRSS2Gen
34+import sqlite3
35+
36+from email.utils import parsedate
37+from datetime import datetime
38+from operator import itemgetter
39+from datetime import timedelta
40+from mailbox import Maildir
41+from urlparse import urljoin
42+
43+from flufl.lock import Lock, TimeOutError
44+from zope.interface import implementer
45+from mailman.interfaces.listmanager import IListManager
46+from zope.component import getUtility
47+
48+from mailman.config import config
49+from mailman.interfaces.archiver import IArchiver
50+
51+log = logging.getLogger('mailman.error')
52+
53+@implementer(IArchiver)
54+class RssArchiver:
55+ name = 'rssarchive'
56+ table_name = 'admin_features'
57+
58+ @staticmethod
59+ def set_length_limit(mlist, length_limit):
60+ rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db')
61+ is_enabled = RssArchiver.is_feed_enabled(mlist)
62+ conn = sqlite3.connect(rss_db_dir)
63+ c = conn.cursor()
64+ sql = ('insert or replace into ' + RssArchiver.table_name +
65+ ' values (\'' + mlist.fqdn_listname + '\', ' + str(is_enabled) +
66+ ', ' + str(length_limit) + ')')
67+ c.execute(sql)
68+ conn.commit()
69+ c.close()
70+ conn.close()
71+
72+ @staticmethod
73+ def set_feed_enabled(mlist, is_enabled):
74+ rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db')
75+ conn = sqlite3.connect(rss_db_dir)
76+ c = conn.cursor()
77+ length_limit = RssArchiver.get_length_limit(mlist)
78+ sql = ('insert or replace into ' + RssArchiver.table_name +
79+ ' values (\'' + mlist.fqdn_listname + '\', ' + str(is_enabled) +
80+ ', ' +str(length_limit) + ')')
81+ c.execute(sql)
82+ conn.commit()
83+ c.close()
84+ conn.close()
85+ if is_enabled:
86+ RssArchiver.generate_rss_feed(mlist)
87+
88+ @staticmethod
89+ def get_length_limit(mlist):
90+ rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db')
91+ RssArchiver.init_database()
92+ conn = sqlite3.connect(rss_db_dir)
93+ c = conn.cursor()
94+ sql = ('select entries_no from ' + RssArchiver.table_name +
95+ ' where list_name = \'' + mlist.fqdn_listname + '\'')
96+ is_enabled = c.execute(sql).fetchone()
97+ if is_enabled == None:
98+ is_enabled = [100]
99+ conn.commit()
100+ c.close()
101+ conn.close()
102+ return is_enabled[0]
103+
104+ @staticmethod
105+ def is_feed_enabled(mlist):
106+ rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db')
107+ RssArchiver.init_database()
108+ conn = sqlite3.connect(rss_db_dir)
109+ c = conn.cursor()
110+ sql = ('select is_switched_on from ' + RssArchiver.table_name +
111+ ' where list_name = \'' + mlist.fqdn_listname + '\'')
112+ is_enabled = c.execute(sql).fetchone()
113+ if is_enabled == None:
114+ is_enabled = [1]
115+ conn.commit()
116+ c.close()
117+ conn.close()
118+ return is_enabled[0]
119+
120+ @staticmethod
121+ def init_database():
122+ rss_db_dir = os.path.join(config.ARCHIVE_DIR, 'rss.db')
123+ conn = sqlite3.connect(rss_db_dir)
124+ c = conn.cursor()
125+ sql = ('create table if not exists ' + RssArchiver.table_name +
126+ ' (list_name text unique, is_switched_on integer, entries_no integer)')
127+ c.execute(sql)
128+ conn.commit()
129+ lists = []
130+ list_manager = getUtility(IListManager)
131+ for fqdn_name in sorted(list_manager.names):
132+ mlist = list_manager.get(fqdn_name)
133+ lists.append(mlist)
134+ for list in lists:
135+ log.error(list.fqdn_listname)
136+ sql = ('insert or ignore into ' + RssArchiver.table_name +
137+ ' values (\'' + list.fqdn_listname + '\', 1, 30)')
138+ c.execute(sql)
139+ conn.commit()
140+ c.close()
141+ conn.close()
142+
143+ @staticmethod
144+ def format_message(msg, msg_as_string):
145+ string_msg = ''
146+ string_msg += 'Date: ' + msg['date'] + '\n'
147+ string_msg += 'From: ' + msg['from'] + '\n'
148+ string_msg += 'To: ' + msg['to'] + '\n\n' + msg_as_string
149+ return string_msg.replace('\n', '<br>').replace(' ', '&nbsp')
150+
151+ @staticmethod
152+ def extract_content(msg):
153+ if msg.is_multipart():
154+ for part in msg.walk():
155+ if part.get_content_type() == 'text/plain':
156+ msg_as_string = part.get_payload + msg_as_string
157+ else:
158+ msg_as_string = msg.get_payload()
159+ return RssArchiver.format_message(msg, msg_as_string)
160+
161+ @staticmethod
162+ def generate_rss_feed(mlist):
163+ archive_dir = os.path.join(config.ARCHIVE_DIR, 'rssarchive')
164+ RssArchiver.create_if_none(archive_dir)
165+ list_dir = os.path.join(archive_dir, mlist.fqdn_listname)
166+ mailbox = Maildir(list_dir, create=True, factory=None)
167+ messages = mailbox.values()
168+ for mes in messages:
169+ mes['parsedate'] = parsedate(mes['date'])
170+ messages.sort(key=itemgetter('parsedate'), reverse=True)
171+ length_limit = RssArchiver.get_length_limit(mlist)
172+ messages = messages[0: length_limit]
173+ rss = PyRSS2Gen.RSS2(
174+ title = mlist.fqdn_listname,
175+ link = "",
176+ description = "The latest messages from: " + mlist.fqdn_listname,
177+ lastBuildDate = datetime.now(),
178+ items = [
179+ PyRSS2Gen.RSSItem(
180+ title = mes['subject'],
181+ description = RssArchiver.extract_content(mes),
182+ pubDate = mes['date'])
183+ for mes in messages
184+ ])
185+ dirPath = os.path.abspath("feeds")
186+ if not os.path.exists(dirPath):
187+ os.makedirs(dirPath)
188+ fileName = mlist.fqdn_listname + '.xml'
189+ filePath = os.path.abspath(os.path.join(dirPath, fileName))
190+ with open(filePath, "w") as f:
191+ rss.write_xml(f)
192+
193+ @staticmethod
194+ def list_url(mlist):
195+ return None
196+
197+ @staticmethod
198+ def permalink(mlist, msg):
199+ return None
200+
201+ @staticmethod
202+ def create_if_none(archive_dir):
203+ try:
204+ os.makedirs(archive_dir, 0775)
205+ except OSError as error:
206+ if error.errno != errno.EEXIST:
207+ raise
208+
209+ @staticmethod
210+ def archive_message(mlist, message):
211+ archive_dir = os.path.join(config.ARCHIVE_DIR, 'rssarchive')
212+ RssArchiver.create_if_none(archive_dir)
213+ list_dir = os.path.join(archive_dir, mlist.fqdn_listname)
214+ mailbox = Maildir(list_dir, create=True, factory=None)
215+ lock_file = os.path.join(
216+ config.LOCK_DIR, '{0}-maildir.lock'.format(mlist.fqdn_listname))
217+ lock = Lock(lock_file)
218+ try:
219+ lock.lock(timeout=timedelta(seconds=1))
220+ mailbox.add(message)
221+ if RssArchiver.is_feed_enabled(mlist):
222+ RssArchiver.generate_rss_feed(mlist);
223+ except TimeOutError:
224+ log.error('Unable to acquire rss archiver lock for {0}, '
225+ 'discarding: {1}'.format(
226+ mlist.fqdn_listname,
227+ message.get('message-id', 'n/a')))
228+ finally:
229+ lock.unlock(unconditionally=True)
230
231=== added file 'src/mailman/archiving/tests/test_rssarchive.py'
232--- src/mailman/archiving/tests/test_rssarchive.py 1970-01-01 00:00:00 +0000
233+++ src/mailman/archiving/tests/test_rssarchive.py 2013-09-01 16:37:50 +0000
234@@ -0,0 +1,203 @@
235+# Copyright (C) 2012-2013 by the Free Software Foundation, Inc.
236+#
237+# This file is part of GNU Mailman.
238+#
239+# GNU Mailman is free software: you can redistribute it and/or modify it under
240+# the terms of the GNU General Public License as published by the Free
241+# Software Foundation, either version 3 of the License, or (at your option)
242+# any later version.
243+#
244+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
245+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
246+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
247+# more details.
248+#
249+# You should have received a copy of the GNU General Public License along with
250+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
251+
252+"""Test the rssarchiver."""
253+
254+from __future__ import absolute_import, print_function, unicode_literals
255+
256+__metaclass__ = type
257+__all__ = [
258+ 'TestRssArchiver',
259+ ]
260+
261+
262+import os
263+import shutil
264+import tempfile
265+import unittest
266+import threading
267+
268+from xml.dom.minidom import parse, parseString
269+from email import message_from_file
270+from flufl.lock import Lock
271+
272+from mailman.app.lifecycle import create_list
273+from mailman.archiving.rssarchive import RssArchiver
274+from mailman.config import config
275+from mailman.database.transaction import transaction
276+from mailman.testing.helpers import LogFileMark
277+from mailman.testing.helpers import (
278+ specialized_message_from_string as mfs)
279+from mailman.testing.layers import ConfigLayer
280+from mailman.utilities.email import add_message_hash
281+
282+
283+class TestRssArchiver(unittest.TestCase):
284+ """Test the rss archiver."""
285+
286+ layer = ConfigLayer
287+
288+ def setUp(self):
289+ # Create a fake mailing list and message object
290+ self._msg = mfs("""\
291+To: test@example.com
292+From: anne@example.com
293+Date: 16-05-2012
294+Subject: Testing the test list
295+Message-ID: <ant>
296+X-Message-ID-Hash: MS6QLWERIJLGCRF44J7USBFDELMNT2BW
297+
298+Tests are better than no tests
299+but the water deserves to be swum.
300+""")
301+ with transaction():
302+ self._mlist = create_list('test@example.com')
303+ # Set up a temporary directory for the prototype archiver so that it's
304+ # easier to clean up.
305+ self._tempdir = tempfile.mkdtemp()
306+ config.push('rssarchive', """
307+ [paths.testing]
308+ archive_dir: {0}
309+ """.format(self._tempdir))
310+ # Capture the structure of a maildir.
311+ self._expected_dir_structure = set(
312+ (os.path.join(config.ARCHIVE_DIR, path) for path in (
313+ 'rssarchive',
314+ os.path.join('rssarchive', self._mlist.fqdn_listname),
315+ os.path.join('rssarchive', self._mlist.fqdn_listname, 'cur'),
316+ os.path.join('rssarchive', self._mlist.fqdn_listname, 'new'),
317+ os.path.join('rssarchive', self._mlist.fqdn_listname, 'tmp'),
318+ )))
319+ self._expected_dir_structure.add(config.ARCHIVE_DIR)
320+
321+ def tearDown(self):
322+ shutil.rmtree(self._tempdir)
323+ config.pop('rssarchive')
324+
325+ def _find(self, path):
326+ all_filenames = set()
327+ for dirpath, dirnames, filenames in os.walk(path):
328+ if not isinstance(dirpath, unicode):
329+ dirpath = unicode(dirpath)
330+ all_filenames.add(dirpath)
331+ for filename in filenames:
332+ new_filename = filename
333+ if not isinstance(filename, unicode):
334+ new_filename = unicode(filename)
335+ all_filenames.add(os.path.join(dirpath, new_filename))
336+ return all_filenames
337+
338+ def test_rssarchiver_xml_file(self):
339+ RssArchiver.archive_message(self._mlist, self._msg)
340+ xml_path = os.path.join('/root/mailman/parts/test/feeds/', (self._mlist.fqdn_listname + '.xml'))
341+
342+ datasource = open(xml_path)
343+ dom = parse(datasource)
344+ rssNode = dom.childNodes[0]
345+ channelNode = rssNode.childNodes[0]
346+
347+ self.assertEqual(len(channelNode.childNodes), 7)
348+
349+ itemNode = channelNode.childNodes[6]
350+ titleNode = itemNode.childNodes[0]
351+ self.assertEqual(titleNode.nodeName, 'title')
352+ self.assertEqual(titleNode.firstChild.nodeValue, 'Testing the test list')
353+
354+ def test_set_length_limit(self):
355+ RssArchiver.set_length_limit(self._mlist, 2)
356+ self.assertEqual(RssArchiver.get_length_limit(self._mlist), 2)
357+
358+ def test_set_feed_enabled(self):
359+ RssArchiver.set_feed_enabled(self._mlist, 1)
360+ self.assertEqual(RssArchiver.is_feed_enabled(self._mlist), 1)
361+ RssArchiver.set_feed_enabled(self._mlist, 0)
362+ self.assertEqual(RssArchiver.is_feed_enabled(self._mlist), 0)
363+
364+ def test_archive_maildir_created(self):
365+ # Archiving a message to the rssarchiver should create the
366+ # expected directory structure.
367+ RssArchiver.archive_message(self._mlist, self._msg)
368+ all_filenames = self._find(config.ARCHIVE_DIR)
369+ # Check that the directory structure has been created and we have one
370+ # more file (the archived message) than expected directories.
371+ archived_messages = [x for x in (all_filenames - self._expected_dir_structure) if 'rss.db' not in x]
372+ self.assertEqual(len(archived_messages), 1)
373+ self.assertTrue(
374+ archived_messages.pop().startswith(
375+ os.path.join(config.ARCHIVE_DIR, 'rssarchive',
376+ self._mlist.fqdn_listname, 'new')))
377+
378+ def test_archive_maildir_existence_does_not_raise(self):
379+ # Archiving a second message does not cause an EEXIST to be raised
380+ # when a second message is archived.
381+ new_dir = None
382+ RssArchiver.archive_message(self._mlist, self._msg)
383+ for directory in ('cur', 'new', 'tmp'):
384+ path = os.path.join(config.ARCHIVE_DIR, 'rssarchive',
385+ self._mlist.fqdn_listname, directory)
386+ if directory == 'new':
387+ new_dir = path
388+ self.assertTrue(os.path.isdir(path))
389+ # There should be one message in the 'new' directory.
390+ self.assertEqual(len(os.listdir(new_dir)), 1)
391+ # Archive a second message. If an exception occurs, let it fail the
392+ # test. Afterward, two messages should be in the 'new' directory.
393+ del self._msg['message-id']
394+ del self._msg['x-message-id-hash']
395+ self._msg['Message-ID'] = '<bee>'
396+ add_message_hash(self._msg)
397+ RssArchiver.archive_message(self._mlist, self._msg)
398+ self.assertEqual(len(os.listdir(new_dir)), 2)
399+
400+ def test_archive_lock_used(self):
401+ # Test that locking the maildir when adding works as a failure here
402+ # could mean we lose mail.
403+ lock_file = os.path.join(
404+ config.LOCK_DIR, '{0}-maildir.lock'.format(
405+ self._mlist.fqdn_listname))
406+ with Lock(lock_file):
407+ # Acquire the archiver lock, then make sure the archiver logs the
408+ # fact that it could not acquire the lock.
409+ archive_thread = threading.Thread(
410+ target=RssArchiver.archive_message,
411+ args=(self._mlist, self._msg))
412+ mark = LogFileMark('mailman.error')
413+ archive_thread.run()
414+ # Test that the archiver output the correct error.
415+ line = mark.readline()
416+ # XXX 2012-03-15 BAW: we really should remove timestamp prefixes
417+ # from the loggers when under test.
418+ self.assertTrue(line.endswith(
419+ 'Unable to acquire rss archiver lock for {0}, '
420+ 'discarding: {1}\n'.format(
421+ self._mlist.fqdn_listname,
422+ self._msg.get('message-id'))))
423+ # Check that the message didn't get archived.
424+ created_files = self._find(config.ARCHIVE_DIR)
425+ self.assertEqual(self._expected_dir_structure, created_files)
426+
427+ def test_rssarchiver_good_path(self):
428+ # Verify the good path; the message gets archived.
429+ RssArchiver.archive_message(self._mlist, self._msg)
430+ new_path = os.path.join(
431+ config.ARCHIVE_DIR, 'rssarchive', self._mlist.fqdn_listname, 'new')
432+ archived_messages = list(os.listdir(new_path))
433+ self.assertEqual(len(archived_messages), 1)
434+ # Check that the email has been added.
435+ with open(os.path.join(new_path, archived_messages[0])) as fp:
436+ archived_message = message_from_file(fp)
437+ self.assertEqual(self._msg.as_string(), archived_message.as_string())
438
439=== modified file 'src/mailman/config/mailman.cfg'
440--- src/mailman/config/mailman.cfg 2013-01-16 23:54:32 +0000
441+++ src/mailman/config/mailman.cfg 2013-09-01 16:37:50 +0000
442@@ -91,3 +91,7 @@
443 class: mailman.runners.digest.DigestRunner
444
445 [style.default]
446+
447+[archiver.rssarchive]
448+class: mailman.archiving.rssarchive.RssArchiver
449+enable: yes
450
451=== modified file 'src/mailman/rest/root.py'
452--- src/mailman/rest/root.py 2013-01-01 14:05:42 +0000
453+++ src/mailman/rest/root.py 2013-09-01 16:37:50 +0000
454@@ -42,6 +42,7 @@
455 from mailman.rest.preferences import ReadOnlyPreferences
456 from mailman.rest.templates import TemplateFinder
457 from mailman.rest.users import AUser, AllUsers
458+from mailman.rest.rss import RssFeed
459
460
461
462
463@@ -136,6 +137,14 @@
464 return AList(list_identifier), segments
465
466 @resource.child()
467+ def feeds(self, request, segments):
468+ """/<api>/feeds/<list>/
469+ """
470+ #return http.ok([], etag({u'segments': segments[0]}))
471+ list_identifier = segments.pop(0)
472+ return RssFeed(list_identifier), segments
473+
474+ @resource.child()
475 def members(self, request, segments):
476 """/<api>/members"""
477 if len(segments) == 0:
478
479=== added file 'src/mailman/rest/rss.py'
480--- src/mailman/rest/rss.py 1970-01-01 00:00:00 +0000
481+++ src/mailman/rest/rss.py 2013-09-01 16:37:50 +0000
482@@ -0,0 +1,72 @@
483+# Copyright (C) 2010-2013 by the Free Software Foundation, Inc.
484+#
485+# This file is part of GNU Mailman.
486+#
487+# GNU Mailman is free software: you can redistribute it and/or modify it under
488+# the terms of the GNU General Public License as published by the Free
489+# Software Foundation, either version 3 of the License, or (at your option)
490+# any later version.
491+#
492+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
493+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
494+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
495+# more details.
496+#
497+# You should have received a copy of the GNU General Public License along with
498+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
499+
500+"""REST for members."""
501+
502+from __future__ import absolute_import, unicode_literals
503+
504+__metaclass__ = type
505+__all__ = [
506+ 'RssAdmin',
507+ ]
508+
509+
510+from uuid import UUID
511+from operator import attrgetter
512+from restish import http, resource
513+from zope.component import getUtility
514+
515+from mailman.app.membership import delete_member
516+from mailman.interfaces.address import InvalidEmailAddressError
517+from mailman.interfaces.listmanager import IListManager, NoSuchListError
518+from mailman.interfaces.member import (
519+ AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError,
520+ NotAMemberError)
521+from mailman.interfaces.subscriptions import ISubscriptionService
522+from mailman.interfaces.user import UnverifiedAddressError
523+from mailman.interfaces.usermanager import IUserManager
524+from mailman.rest.helpers import (
525+ CollectionMixin, PATCH, etag, no_content, paginate, path_to)
526+from mailman.rest.preferences import Preferences, ReadOnlyPreferences
527+from mailman.rest.validator import (
528+ Validator, enum_validator, subscriber_validator)
529+from mailman.archiving.rssarchive import RssArchiver
530+
531+class RssFeed(resource.Resource):
532+ def __init__(self, list_identifier):
533+ # list-id is preferred, but for backward compatibility, fqdn_listname
534+ # is also accepted. If the string contains '@', treat it as the
535+ # latter.
536+ manager = getUtility(IListManager)
537+ if '@' in list_identifier:
538+ self._mlist = manager.get(list_identifier)
539+ else:
540+ self._mlist = manager.get_by_list_id(list_identifier)
541+
542+ @resource.GET()
543+ def getData(self, request):
544+ is_feed_enabled = RssArchiver.is_feed_enabled(self._mlist)
545+ size = RssArchiver.get_length_limit(self._mlist)
546+ return http.ok([], etag({u'enabled': is_feed_enabled, u'sizeLimit': size}))
547+
548+ @resource.POST()
549+ def setData(self, request):
550+ length_limit = request.POST.get('size_limit')
551+ is_enabled = request.POST.get('is_enabled')
552+ RssArchiver.set_length_limit(self._mlist, length_limit)
553+ RssArchiver.set_feed_enabled(self._mlist, is_enabled)
554+ return no_content()