e4caec2915085db556b9e7ca12b9d74ca01ffc8b
[zzz-spline-frontpage.git] / splinext / frontpage / __init__.py
1 from collections import namedtuple
2 import datetime
3 from pkg_resources import resource_filename
4 import subprocess
5
6 import feedparser
7 import lxml.html
8
9 from spline.lib import helpers
10 from spline.lib.plugin import PluginBase, PluginLink, Priority
11
12 import splinext.frontpage.controllers.frontpage
13
14 class FrontPageUpdate(object):
15 """Base class ('interface') for an updated thing that may appear on the
16 front page.
17
18 Subclasses should implement the `time` and `template` properties.
19 """
20 pass
21
22
23 RSS_SUMMARY_LENGTH = 1000
24
25 FrontPageRSS = namedtuple('FrontPageRSS',
26 ['time', 'entry', 'template', 'category', 'content', 'icon'])
27
28 def rss_hook(limit, max_age, url, title=None, icon=None):
29 """Front page handler for news feeds."""
30 feed = feedparser.parse(url)
31
32 if not title:
33 title = feed.feed.title
34
35 updates = []
36 for entry in feed.entries[:limit]:
37 # Grab a date -- Atom has published, RSS usually just has updated.
38 # Both come out as time tuples, which datetime.datetime() can read
39 try:
40 timestamp_tuple = entry.published_parsed
41 except AttributeError:
42 timestamp_tuple = entry.updated_parsed
43 timestamp = datetime.datetime(*timestamp_tuple[:6])
44
45 if max_age and timestamp < max_age:
46 # Entries should be oldest-first, so we can bail after the first
47 # expired entry
48 break
49
50 # Try to find something to show! Default to the summary, if there is
51 # one, or try to generate one otherwise
52 content = u''
53 if 'summary' in entry:
54 # If there be a summary, cheerfully trust that it's actually a
55 # summary
56 content = entry.summary
57 elif 'content' in entry:
58 # Full content is way too much, especially for my giant blog posts.
59 # Cut this down to some arbitrary number of characters, then feed
60 # it to lxml.html to fix tag nesting
61 broken_html = entry.content[0].value[:RSS_SUMMARY_LENGTH]
62 fragment = lxml.html.fromstring(broken_html)
63
64 # Insert an ellipsis at the end of the last node with text
65 last_text_node = None
66 last_tail_node = None
67 # Need to find the last node with a tail, OR the last node with
68 # text if it's later
69 for node in fragment.iter():
70 if node.tail:
71 last_tail_node = node
72 last_text_node = None
73 elif node.text:
74 last_text_node = node
75 last_tail_node = None
76
77 if last_text_node is not None:
78 last_text_node.text += '...'
79 if last_tail_node is not None:
80 last_tail_node.tail += '...'
81
82 # Serialize
83 content = lxml.html.tostring(fragment)
84
85 content = helpers.literal(content)
86
87 update = FrontPageRSS(
88 time = timestamp,
89 entry = entry,
90 template = '/front_page/rss.mako',
91 category = title,
92 content = content,
93 icon = icon,
94 )
95 updates.append(update)
96
97 return updates
98
99
100 FrontPageGit = namedtuple('FrontPageGit',
101 ['time', 'gitweb', 'log', 'tag', 'template', 'category', 'icon'])
102 FrontPageGitCommit = namedtuple('FrontPageGitCommit',
103 ['hash', 'author', 'time', 'subject', 'repo'])
104
105 def git_hook(limit, max_age, title, gitweb, repo_paths, repo_names,
106 tag_pattern=None, icon=None):
107
108 """Front page handler for repository history."""
109 # Repo stuff can be space-delimited lists...
110 repo_paths = repo_paths.split()
111 repo_names = repo_names.split()
112
113 # Fetch the main repo's git tags
114 args = [
115 'git',
116 '--git-dir=' + repo_paths[0],
117 'tag', '-l',
118 ]
119 if tag_pattern:
120 args.append(tag_pattern)
121
122 proc = subprocess.Popen(args, stdout=subprocess.PIPE)
123 git_output, _ = proc.communicate()
124 tags = git_output.strip().split('\n')
125
126 # Tags come out in alphabetical order, which means earliest first. Reverse
127 # it to make the slicing easier
128 tags.reverse()
129 # Only history from tag to tag is actually interesting, so get the most
130 # recent $limit tags but skip the earliest
131 interesting_tags = tags[:-1][:limit]
132
133 updates = []
134 for tag, since_tag in zip(interesting_tags, tags[1:]):
135 # Get the date when this tag was actually created
136 args = [
137 'git',
138 '--git-dir=' + repo_paths[0],
139 'for-each-ref',
140 '--format=%(taggerdate:raw)',
141 'refs/tags/' + tag,
142 ]
143 tag_timestamp, _ = subprocess.Popen(args, stdout=subprocess.PIPE) \
144 .communicate()
145 tag_unixtime, tag_timezone = tag_timestamp.split(None, 1)
146 tagged_timestamp = datetime.datetime.fromtimestamp(int(tag_unixtime))
147
148 if max_age and tagged_timestamp < max_age:
149 break
150
151 commits = []
152
153 for repo_path, repo_name in zip(repo_paths, repo_names):
154 # Grab an easily-parsed history: fields delimited by nulls.
155 # Hash, author's name, commit timestamp, subject.
156 git_log_args = [
157 'git',
158 '--git-dir=' + repo_path,
159 'log',
160 '--pretty=%h%x00%an%x00%at%x00%s',
161 "{0}..{1}".format(since_tag, tag),
162 ]
163 proc = subprocess.Popen(git_log_args, stdout=subprocess.PIPE)
164 for line in proc.stdout:
165 hash, author, time, subject = line.strip().split('\x00')
166 commits.append(
167 FrontPageGitCommit(
168 hash = hash,
169 author = author,
170 time = datetime.datetime.fromtimestamp(int(time)),
171 subject = subject,
172 repo = repo_name,
173 )
174 )
175
176 update = FrontPageGit(
177 time = tagged_timestamp,
178 gitweb = gitweb,
179 log = commits,
180 template = '/front_page/git.mako',
181 category = title,
182 tag = tag,
183 icon = icon,
184 )
185 updates.append(update)
186
187 return updates
188
189
190 def add_routes_hook(map, *args, **kwargs):
191 """Hook to inject some of our behavior into the routes configuration."""
192 map.connect('/', controller='frontpage', action='index')
193
194
195 class FrontPagePlugin(PluginBase):
196 def controllers(self):
197 return dict(
198 frontpage = splinext.frontpage.controllers.frontpage.FrontPageController,
199 )
200
201 def template_dirs(self):
202 return [
203 (resource_filename(__name__, 'templates'), Priority.FIRST)
204 ]
205
206 def hooks(self):
207 return [
208 ('routes_mapping', Priority.NORMAL, add_routes_hook),
209 ('frontpage_updates_rss', Priority.NORMAL, rss_hook),
210 ('frontpage_updates_git', Priority.NORMAL, git_hook),
211 ]