Commit 471f6f7b4135d004be040c2bbd552b12451badfe

Initial import of a Python library for OCS services. Currently the backend
can read forums, threads and posts. Posting support will be added later.
  
1The OCS client for the forums is located here.
  
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2010 Luca Beltrame <einar@heavensinferno.net>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License, under
8# version 2 of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details
14#
15# You should have received a copy of the GNU General Public
16# License along with this program; if not, write to the
17# Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20"""This module contains classes that act as containers for the OCS XML
21responses. There are containers for forum lists, forum threads, and posts.
22They are used by all the other parts of the client."""
23
24import dateutil.parser as dateparser
25import lxml.etree as etree
26
27class Forum(object):
28
29 "Class representing forums."
30
31 def __init__(self, element):
32
33 self._id = element.find("id")
34 self._name = element.find("name")
35 self._description = ''.join(map(etree.tostring,
36 element.find("description")))
37 self._icon = element.find("icon")
38 self._parent = element.find("parent")
39 self._itemcount = element.find("itemcount")
40
41 @property
42 def id(self):
43
44 "Return the ID of the forum."
45
46 return self._id.text
47
48 @property
49 def name(self):
50
51 "Return the name of the forum."
52
53 return self._name.text
54
55 @property
56 def description(self):
57
58 "Return the description of the forum."
59
60 return self._description
61
62 @property
63 def icon(self):
64
65 "Return the URL to icon of the forum, if present."
66
67 return self._icon.text
68
69 @property
70 def parent(self):
71
72 "Return the ID of the parent forum."
73
74 return self._parent.text
75
76 @property
77 def posts(self):
78
79 "Return the number of posts of the forum."
80
81 return self._itemcount.text
82
83class ForumThread(object):
84
85 "Class representing forum threads."
86
87 def __init__(self, element):
88
89 self._id = element.find("id")
90 self._category = element.find("category")
91 self._poster = element.find("user")
92 self._timestamp = element.find("changed")
93 self._subject = element.find("name")
94 self._description = ''.join(map(etree.tostring,
95 element.find("description")))
96 self._comment_no = element.find("comments")
97
98 @property
99 def id(self):
100
101 "Return the ID of the current thread"
102
103 return self._id.text
104
105 @property
106 def forum_id(self):
107
108 "Return the original forum ID of the thread"
109
110 return self._category.text
111
112 @property
113 def date(self):
114
115 "Return the date of the the thread"
116
117 date = dateparse.parse(self.__timestamp.text)
118 date = date.strftime("%c")
119
120 return date
121
122 @property
123 def poster(self):
124
125 "Return the user name of the first poster of the thread."
126
127 return self._poster.text
128
129 @property
130 def subject(self):
131
132 "Return the subject of the thread."
133
134 return self._subject.text
135
136 @property
137 def description(self):
138
139 "Return the first post of the thread."
140
141 return self._description
142
143 @property
144 def replies(self):
145
146 "Return the number of replies."
147
148 return self._comment_no.text
149
150
151class Message(object):
152
153 """Class representing forum messages. Notice that this is used to represent
154 all messages in a thread but the first one: use the ParentMessage for that.
155 """
156
157 def __init__(self, element):
158
159 element_data = element.getchildren()
160
161 self._temp = element_data
162
163 self._id = element_data[0]
164 self._subject = element_data[1]
165 self._text = ''.join(map(etree.tostring, element_data[2]))
166 self._childcount = element_data[3]
167 self._user = element_data[4]
168 self._date = element_data[5]
169
170 @property
171 def id(self):
172
173 "Return the ID of the message."
174
175 return self._id.text
176
177 @property
178 def subject(self):
179
180 "Return the subject of the message."
181
182 return self._subject.text
183
184 @property
185 def text(self):
186
187 "Return the message text."
188
189 return self._text
190
191 @property
192 def replies(self):
193
194 "Return the number of replies."
195
196 return self._childcount.text
197
198 @property
199 def user(self):
200
201 "Return the name of the poster."
202
203 return self._user.text
204
205 @property
206 def date(self):
207
208 "Return the date of the message."
209
210 date = dateparse.parse(self._date.text)
211 date = date.strftime("%c")
212
213 return date
214
215
216class ParentMessage(Message):
217
218 """Sub-class of Message used to represent the first post of a thread. The
219 rationale is that OCS puts the first post in a totally different location
220 than the others."""
221
222 def __init__(self, element):
223
224 element_data = element.getchildren()
225
226 self._id = element_data[0]
227 self._category = element_data[1]
228 self._user = element_data[2]
229 self._date = element_data[3]
230 self._subject = element_data[4]
231 self._text = ''.join(map(etree.tostring, element_data[5]))
232 self._childcount = element_data[6]
  
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2010 Luca Beltrame <einar@heavensinferno.net>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License, under
8# version 2 of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details
14#
15# You should have received a copy of the GNU General Public
16# License along with this program; if not, write to the
17# Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20"""This module contains the class used to interact directly with Open
21Collaboration Services, OCService."""
22
23import urlparse
24
25import lxml.etree as etree
26
27import containers
28
29class OCService(object):
30
31 """Class to handle Open Collaboration Services (OCS) as implemented in the
32 KDE Community Forums. It is used to query forums, threads and messages,
33 and to make new posts.
34
35 Example usage (to obtain a list of all forums):
36
37 >>> url = "http://forum.kde.org/"
38 >>> ocs_request = OCService(url)
39 >>> all_forums = ocs_request.list_forums()
40
41 """
42
43 _LIST_FORUMS_URL = "ocs/knowledgebase/categories"
44 _ADD_POST_URL = "ocs/knowledgebase/add"
45 _LIST_TOPICS_URL = "ocs/knowledgebase/data"
46 _COMMENTS_URL = "ocs/comments/data/4"
47
48 def __init__(self, url, username=None, password=None):
49
50 url = ''.join((url, "/")) if not url.endswith("/") else url
51 self.url = url
52 self._username = username
53 self._password = password
54
55 def _url_exists(self, url):
56
57 "Check if a given URL exists."
58
59 pass
60
61 def _check_status(self, data):
62
63 """Check the status of an OCS operation. If it is not OK, raise an
64 IOError exception.
65
66 Arguments:
67 - data - the ElementTree instances obtained from parsing
68
69 """
70
71 # The status is held in the "meta" section of the OCS response
72 parsed_response = data.xpath("//meta")[0]
73 status = parsed_response[0].text
74 statuscode = parsed_response[1].text
75 message = parsed_response[2].text
76
77 if status != "ok":
78 error = ' '.join((statuscode, message))
79 raise IOError(error)
80
81 def list_forums(self):
82
83 """List all available forums.
84
85 Return:
86 - A list of ForumCategory instances for all forums in the board
87 """
88
89 request_url = urlparse.urljoin(self.url, self._LIST_FORUMS_URL)
90 print request_url
91 result = etree.parse(request_url)
92 root = result.getroot()
93 self._check_status(root)
94 data = root[1]
95
96 category_list = list()
97
98 for element in data.iterchildren():
99 category_data = containers.Forum(element)
100 category_list.append(category_data)
101
102 return category_list
103
104 def list_forum_threads(self, forum_id):
105
106 """List threads belonging to a specific forum.
107
108 Arguments:
109 - forum_id - The forum id to list threads from
110 Return:
111 - A list of ForumThread instances
112
113 """
114
115 request_id = "?category=%s" % forum_id
116 request_id = ''.join((self._LIST_TOPICS_URL, request_id))
117 request_url = urlparse.urljoin(self.url, request_id)
118
119 result = etree.parse(request_url)
120 root = result.getroot()
121 self._check_status(root)
122
123 data = root[1]
124
125 post_list = list()
126
127 for element in data.iterchildren():
128 post_data = containers.ForumThread(element)
129 post_list.append(post_data)
130
131 return post_list
132
133 def _get_parent_message(self, forum_id, topic_id):
134
135 """Get the parent message of a thread. This is needed because the OCS
136 comment URL does not contain the parent message, which is instead
137 retrieved from knowledgebase/data. The correct topic ID is identified
138 through XPath.
139
140 Arguments:
141 - forum_id - the forum id to retrieve the message from
142 - topic_id - the thread id to retrieve the message from
143
144 Return:
145 - a ParentMessage instance containing the message
146
147 """
148
149 request_id = "?category=%s" % forum_id
150 request_id = ''.join((self._LIST_TOPICS_URL, request_id))
151 request_url = urlparse.urljoin(self.url, request_id)
152
153 result = etree.parse(request_url)
154 root = result.getroot()
155
156 response = root[0]
157 data = root[1]
158
159 result = data.xpath("//data/content[id=%d]" % topic_id)
160 if result:
161 parent = containers.ParentMessage(result[0])
162 return parent
163
164 def show_thread(self, forum_id, topic_id):
165
166 """Show a specific thread (identified by a topic id) in a specific forum
167 (identified by forum id). First the parent thread is retrieved, then
168 the children, in succession.
169
170 Arguments:
171 - forum_id - the forum id to retrieve posts from
172 - topic_id - the thread id to retrieve posts from
173
174 Return:
175 - a list of Message instances, with the exception of the first
176 element, which is a ParentMessage instance
177
178 """
179
180 request_id = "/%d/%d" % (forum_id, topic_id)
181 request_id = ''.join((self._COMMENTS_URL, request_id))
182 request_url = urlparse.urljoin(self.url, request_id)
183
184 result = etree.parse(request_url)
185 root = result.getroot()
186 self._check_status(root)
187
188 data = root[1]
189
190 message_list = list()
191 parent = self._get_parent_message(forum_id, topic_id)
192 message_list.append(parent)
193
194 for element in data.iterchildren():
195 message_data = containers.Message(element)
196 message_list.append(message_data)
197
198 return message_list
199
200 def post(self, forum_id, subject, message):
201
202 "Posts a message to a specific board."
203 # TODO
204 pass