├── Datadumps
├── Arduino
│ ├── Badges.xml
│ ├── Comments.xml
│ ├── PostHistory.xml
│ ├── PostLinks.xml
│ ├── Posts.xml
│ ├── Tags.xml
│ ├── Users.xml
│ └── Votes.xml
├── Astronomy
│ ├── Badges.xml
│ ├── Comments.xml
│ ├── PostHistory.xml
│ ├── PostLinks.xml
│ ├── Posts.xml
│ ├── Tags.xml
│ ├── Users.xml
│ └── Votes.xml
└── Beer
│ ├── Badges.xml
│ ├── Comments.xml
│ ├── PostHistory.xml
│ ├── PostLinks.xml
│ ├── Posts.xml
│ ├── Tags.xml
│ ├── Users.xml
│ └── Votes.xml
├── LICENSE
├── README.md
├── Sources
├── Indexer.py
├── SearchEngine.py
├── WebServer.py
├── static
│ ├── fonts
│ │ ├── OpenSans-Regular.eot
│ │ ├── OpenSans-Regular.otf
│ │ ├── OpenSans-Regular.svg
│ │ ├── OpenSans-Regular.ttf
│ │ ├── OpenSans-Regular.woff
│ │ ├── Play-Regular.eot
│ │ ├── Play-Regular.otf
│ │ ├── Play-Regular.svg
│ │ ├── Play-Regular.ttf
│ │ └── Play-Regular.woff
│ ├── images
│ │ ├── bg-adbox.jpg
│ │ ├── bg-button.png
│ │ ├── box-of-icons.png
│ │ ├── box.png
│ │ ├── divider.png
│ │ ├── icons.png
│ │ ├── logo.png
│ │ └── recycle.png
│ └── style.css
└── views
│ ├── about.tpl
│ ├── contact.tpl
│ ├── footer.tpl
│ ├── full_doc.tpl
│ ├── header.tpl
│ ├── help.tpl
│ ├── index.tpl
│ ├── news.tpl
│ ├── search_results.tpl
│ └── site.tpl
└── requirements.txt
/Datadumps/Arduino/PostLinks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/Datadumps/Arduino/Tags.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/Datadumps/Astronomy/PostLinks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
--------------------------------------------------------------------------------
/Datadumps/Astronomy/Tags.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
--------------------------------------------------------------------------------
/Datadumps/Beer/PostLinks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/Datadumps/Beer/Tags.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Intranet Stack Exchange
3 | -----------------------
4 | A tool to enable using the [StackExchange datadumps](https://archive.org/details/stackexchange)
5 | in offline environments (e.g. internal network of a company, not connected to the
6 | internet).
7 |
8 | Here are a few screenshots:
9 |
10 | *
11 | *
12 | *
13 |
14 | This application will enable organizations, whose network is isolated from the web, to enjoy
15 | the massive Q&A database StackExchange has publicly released, in various topics ranging from
16 | Programming to Movies to Math and much more.
17 |
18 | Questions, ideas and bugs - please contact me: ran@ranlevi.com
19 |
20 | ## Prerequisites:
21 | * Python 2.7
22 | * A Linux server with admin privileges
23 |
24 |
25 | ## Dependencies:
26 |
27 | * [Bottle](http://bottlepy.org/docs/dev/index.html)
28 | * [Whoosh](https://pypi.python.org/pypi/Whoosh)
29 | * [CherryPy](www.cherrypy.org)
30 |
31 | * Known issues: None.
32 |
33 | ## Basic Usage:
34 |
35 | * Clone (or Unzip) the git to some directory.
36 | * install the required libraries: either run "pip install -r requirements.txt" for
37 | automatic install, or install the libraries (Whoosh, bootle, etc.) manually.
38 | * Go to the /Sources directory
39 | * Run:
40 |
41 | python Indexer.py
42 |
43 | The app will index all the datadumps in the /Datadumps folder.
44 |
45 | * Run:
46 |
47 | python Webserver.py
48 |
49 | The webserver will start.
50 |
51 | If no IP or Port number were given, the server will launch in 'development mode':
52 | localhost, 8080.
53 |
54 | Open the web browswer, and go to the server's ip address. e.g.:
55 | "http://192.1.2.3:80/" or "http://localhost:8080/"
56 | You will get a browesable version of the Stack Exchange sites.
57 |
58 | ## Advanced Options:
59 |
60 | 1. The application comes with 3 sample databases: Ardunio, Astronomy and Beer. To add
61 | more databases, go to the Internet-Archive download page for data-dumps
62 | (https://archive.org/details/stackexchange), and download the databases you want.
63 | Unzip the downloaded databases into the /Datadumps folder, under a folder with the database's name.
64 |
65 | For example:
66 |
67 | /Datadumps
68 | /Beer
69 | /Astronomy
70 | /Ardunio
71 | /Movies
72 | Posts.xml
73 | Comments.xml
74 | ...
75 |
76 | 2. It is possible to index only selected database(s), to save time. in /Sources, type: python Indexer.py debug
77 |
78 | 3. The website I used for the application is minimal and basic. You can modify it easily by
79 | changing the HTML, CSS, etc. in the /Sources/views and /Sources/static folders.
80 | Alternatively, you can design you're own website from scratch! The Webserver.py module calls functions
81 | in the Indexer.py & SearchEngine.py modules - so you can modify only the Webserver.py module for a whole
82 | new user experience.
83 |
84 | **Note:**
85 | This work is partly inspired by Stackdump (https://bitbucket.org/samuel.lai/stackdump). Thanks, Samuel! :-)
86 |
--------------------------------------------------------------------------------
/Sources/Indexer.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as ET
2 | from whoosh.fields import Schema, TEXT, KEYWORD
3 | from whoosh.index import create_in
4 | import shelve, os, sys
5 | import cPickle
6 |
7 | """
8 | Internal Stack Exchange - Indexer
9 | ---------------------------------
10 | This module scans the available datadumps in the /Datadumps directory,
11 | and parses the .xml files it finds their.
12 | It then builds a database that holds 'docs'. A 'doc' is a full
13 | question + answers dataset, and is referenced by it's question Id.
14 |
15 | It also creates a search engine index (using the Woosh library), that
16 | allows the user to index and search for docs using tags, keywords, etc.
17 |
18 | """
19 | #####################################################################
20 | #####################################################################
21 | def parse_xmls(path_to_xmls, site_name):
22 | """ Create a file .db under the folder /db/.
23 | The file is a shelve, containing full docs of the named site. Key is the question id.
24 |
25 | Note: this function usage of Shelve is optimzed for memory use, since the S.E datadumps
26 | can be huge.
27 | All the .xml files are parsed into Shelves, so that their content will
28 | not need to stay in memory. The shelve is opened and read only when needed.
29 | """
30 |
31 | ###################################################################################
32 | #Create a shelve to hold users' info: {user id : user info}
33 | shlv = shelve.open('../temp_db/tmp_users.db', 'n', protocol = -1, writeback = False)
34 |
35 | #Memory efficient method, allows for clearing the root.
36 | context = ET.iterparse(path_to_xmls + 'Users.xml', events = ('start', 'end'))
37 | context = iter(context)
38 | event, root = context.next() #get root
39 |
40 | print ("******* Starting Users.xml parsing ******")
41 | i = 0
42 | for (event, user) in context:
43 | if event == 'end' and user.tag=='row':
44 | shlv[user.attrib['Id']] = user.attrib
45 |
46 | #Log out progress to the caller.
47 | i += 1
48 | if i%5000==0:
49 | shlv.sync() #Syncing the shelve clears the cache, and frees the memory.
50 | print ("Processed {0} users so far.".format(i))
51 | root.clear()
52 |
53 | shlv.close()
54 |
55 | ###################################################################################
56 | #Create a shelve to hold RelatedPosts info: {post id (id of relevant post) : list of related post id}
57 | shlv = shelve.open('../temp_db/tmp_related_posts.db', 'n', protocol = -1, writeback = False)
58 |
59 | #Memory efficient method
60 | context = ET.iterparse(path_to_xmls + 'PostLinks.xml', events = ('start', 'end'))
61 | context = iter(context)
62 | event, root = context.next() #get root
63 |
64 | print ("******* Starting PostLinks.xml parsing ******")
65 | i = 0
66 | for (event, postlink) in context:
67 | if event == 'end' and postlink.tag=='row':
68 |
69 | #Check if the shelve already has the post_id key, and if not - create a new one.
70 | post_id = postlink.attrib['PostId']
71 | list_of_related_links = shlv.get(post_id, [])
72 | list_of_related_links.append(postlink.attrib)
73 | shlv.update({post_id: list_of_related_links})
74 |
75 | #Log out progress to the user.
76 | i += 1
77 | if i%5000==0:
78 | shlv.sync()
79 | print ("Processed {0} PostLinks so far.".format(i))
80 | root.clear()
81 |
82 | shlv.close()
83 |
84 | ###################################################################################
85 | #Create a shelve to hold comments info: {post id : list of comments}
86 | shlv = shelve.open('../temp_db/tmp_comments.db', 'n', protocol = -1, writeback = True)
87 |
88 | #This shlv holds the user data.
89 | tmp_users_shlv = shelve.open('../temp_db/tmp_users.db', 'r', protocol = -1)
90 |
91 | #Memory efficient method
92 | context = ET.iterparse(path_to_xmls + 'Comments.xml', events = ('start', 'end'))
93 | context = iter(context)
94 | event, root = context.next()
95 |
96 | print ("******* Starting Comments.xml parsing ******")
97 | i = 0
98 | for (event, comment) in context:
99 | if event == 'end' and comment.tag=='row':
100 |
101 | if 'UserId' in comment.attrib.keys():
102 | #If the comment has a userId, we try to find the user's details
103 | #in the tmp_users_shlv. If there is none, we keep the field empty.
104 | user_id = comment.attrib.get('UserId', '')
105 | user_data = tmp_users_shlv.get(user_id, '')
106 | comment.attrib.update({'User': user_data})
107 |
108 | post_id = comment.attrib['PostId']
109 |
110 | list_of_comments = shlv.get(post_id, [])
111 | list_of_comments.append(comment.attrib)
112 | shlv.update({post_id:list_of_comments})
113 |
114 | #Log out progress to the user.
115 | i += 1
116 | if i%10000==0:
117 | shlv.sync()
118 | print ("Processed {0} Comments so far.".format(i))
119 | root.clear()
120 |
121 | tmp_users_shlv.close()
122 | shlv.close()
123 |
124 | ###################################################################################
125 | #Create a shelve to hold questions info only: {post id: post info}
126 | #Same for answers, but structure is: {parent id: list of posts}
127 | tmp_questions_shlv = shelve.open('../temp_db/tmp_questions.db', 'n', protocol = -1, writeback = True)
128 | tmp_answers_shlv = shelve.open('../temp_db/tmp_answers.db', 'n', protocol = -1, writeback = True)
129 |
130 | tmp_comments_shlv = shelve.open('../temp_db/tmp_comments.db', 'r', protocol = -1)
131 | tmp_users_shlv = shelve.open('../temp_db/tmp_users.db', 'r', protocol = -1)
132 | tmp_postlinks_shlv = shelve.open('../temp_db/tmp_related_posts.db', 'r', protocol = -1)
133 |
134 | context = ET.iterparse(path_to_xmls + 'Posts.xml', events = ('start', 'end'))
135 | context = iter(context)
136 | event, root = context.next()
137 |
138 | print ("******* Starting Posts.xml parsing ******")
139 | i = 0
140 | for (event, post) in context:
141 | if event == 'end' and post.tag == 'row':
142 |
143 | if (post.attrib['PostTypeId']=='1'):#A question
144 | tmp_questions_shlv[post.attrib['Id']] = post.attrib
145 |
146 | elif (post.attrib['PostTypeId']=='2'):#An Answer
147 |
148 | if 'OwnerUserId' in post.attrib.keys():
149 | #If we have the user details, add them to the answer.
150 | user_id = post.attrib['OwnerUserId']
151 | user_data = tmp_users_shlv.get(user_id, '')
152 | post.attrib.update({'User': user_data})
153 |
154 | post_id = post.attrib['Id']
155 |
156 | list_of_postlinks = tmp_postlinks_shlv.get(post_id, [])
157 | post.attrib.update({'PostLinks': list_of_postlinks})
158 |
159 | list_of_comments = tmp_comments_shlv.get(post_id, [])
160 | post.attrib.update({'Comments': list_of_comments})
161 |
162 | parent_id = post.attrib['ParentId']
163 |
164 | list_of_answers = tmp_answers_shlv.get(parent_id, [])
165 | list_of_answers.append(post.attrib)
166 | tmp_answers_shlv.update({parent_id:list_of_answers})
167 |
168 | i += 1
169 | if i%5000==0:
170 | tmp_questions_shlv.sync()
171 | tmp_answers_shlv.sync()
172 | print ("Processed {0} Posts so far.".format(i))
173 |
174 | root.clear()
175 |
176 | tmp_postlinks_shlv.close()
177 | tmp_users_shlv.close()
178 | tmp_comments_shlv.close()
179 | tmp_questions_shlv.close()
180 | tmp_answers_shlv.close()
181 |
182 |
183 | ####################################################################################
184 | # Create the shelve that will hold the full documents. {question id : doc}
185 | full_docs_shlv = shelve.open('../db/' + site_name +'.db', 'n', protocol = -1, writeback = True)
186 |
187 | tmp_posts_shlv = shelve.open('../temp_db/tmp_questions.db', 'r', protocol = -1)
188 | tmp_users_shlv = shelve.open('../temp_db/tmp_users.db', 'r', protocol = -1)
189 | tmp_answers_shlv = shelve.open('../temp_db/tmp_answers.db', 'r', protocol = -1)
190 | tmp_comments_shlv = shelve.open('../temp_db/tmp_comments.db', 'r', protocol = -1)
191 | tmp_postlinks_shlv = shelve.open('../temp_db/tmp_related_posts.db', 'r', protocol = -1)
192 |
193 | print ("******* Now creating full docs ******")
194 | i = 0
195 | num_of_docs = len(tmp_posts_shlv.keys())
196 |
197 | for id in tmp_posts_shlv.keys():
198 |
199 | doc_template = {'Comments' : [],
200 | 'PostLinks' : [],
201 | 'Answers' : [],
202 | 'User' : '',
203 | 'AcceptedAnswerId' : '',
204 | 'Body' : '',
205 | 'OwnerUserId' : '',
206 | 'Title' : '',
207 | 'Tags' : '',
208 | 'Score' : ''
209 | }
210 |
211 | doc_template['Title'] = tmp_posts_shlv[id]['Title']
212 | doc_template['Tags'] = tmp_posts_shlv[id]['Tags']
213 | doc_template['Body'] = tmp_posts_shlv[id]['Body']
214 | doc_template['Score'] = tmp_posts_shlv[id]['Score']
215 |
216 | #return default value '' if none
217 | doc_template['AcceptedAnswerId'] = tmp_posts_shlv[id].get('AcceptedAnswerId', '')
218 | doc_template['OwnerUserId'] = tmp_posts_shlv[id].get('OwnerUserId', '')
219 | doc_template['User'] = tmp_users_shlv.get(doc_template['OwnerUserId'], '')
220 |
221 | #get all the comments, answers and postlinks. Return empty list if none.
222 | doc_template['Comments'] = tmp_comments_shlv.get(id, [])
223 | doc_template['Answers'] = tmp_answers_shlv.get(id, [])
224 | doc_template['PostLinks'] = tmp_postlinks_shlv.get(id, [])
225 |
226 | full_docs_shlv[id] = doc_template
227 |
228 | i += 1
229 | if i%1000==0:
230 | full_docs_shlv.sync()
231 | print ("Processed {0} Full Docs out of {1}.".format(i, num_of_docs))
232 |
233 |
234 | tmp_posts_shlv.close()
235 | tmp_users_shlv.close()
236 | full_docs_shlv.close()
237 | tmp_answers_shlv.close()
238 | tmp_comments_shlv.close()
239 | tmp_postlinks_shlv.close()
240 |
241 |
242 | #####################################################################
243 | #####################################################################
244 | def create_schema(path_to_index_folder, db_name):
245 | """ Create a schema for the whoosh index. Return a pointer to the created index.
246 | """
247 | #The schema will hold the texts of a full document,
248 | #the tags (As a comma seperated list) and the id of the question.
249 | db_docs_schema = Schema(doc_texts = TEXT(),
250 | doc_tags = KEYWORD(commas = True, scorable = True),
251 | question_id = TEXT(stored = True))
252 |
253 | db_docs_ix_pointer = create_in(path_to_index_folder,
254 | schema = db_docs_schema,
255 | indexname = db_name + '_index')
256 | return db_docs_ix_pointer
257 |
258 | #####################################################################
259 | #####################################################################
260 | #@profile
261 | def index_data(db_docs_ix_pointer, site_name):
262 | """ Do the search engine indexing of a data.
263 | """
264 | doc_writer = db_docs_ix_pointer.writer(limitmb = 512, procs = 2)
265 |
266 | full_docs_shlv = shelve.open('../db/' + site_name +'.db', 'r', protocol = -1)
267 |
268 | num_of_docs = len(full_docs_shlv.keys())
269 | i = 0
270 |
271 | print ("Now Indexing {0}".format(site_name))
272 | for qid in full_docs_shlv.keys():
273 |
274 | #Extract all the texts from a document.
275 | tmp_text = ''
276 | tmp_text += full_docs_shlv[qid]['Title'] + ''
277 | tmp_text += full_docs_shlv[qid]['Body'] + ''
278 |
279 | tmp_text += ' '.join([comment['Text'] for comment in full_docs_shlv[qid]['Comments']]) + ' '
280 | tmp_text += ' '.join([answer['Body'] for answer in full_docs_shlv[qid]['Answers']]) + ' '
281 |
282 | for answer in full_docs_shlv[qid]['Answers']:
283 | tmp_text += ' '.join([ans_comment['Text'] for ans_comment in answer['Comments']]) + ' '
284 |
285 | #Convert the tags from the form to ['aa','bb']
286 | tmp_tags = full_docs_shlv[qid]['Tags']
287 | l = tmp_tags.split("><")
288 | fixed_tags = [tag.replace("<", "").replace(">","") for tag in l]
289 | fixed_tags = unicode(",".join(fixed_tags))
290 |
291 | doc_writer.add_document(doc_texts = unicode(tmp_text),
292 | doc_tags = fixed_tags,
293 | question_id = unicode(qid))
294 |
295 | #Display a progress report to the user.
296 | i+=1
297 | if (i%100 == 0):
298 | print ("Indexed doc {0} out of {1}".format(i,num_of_docs))
299 |
300 | db_docs_ix_pointer.close()
301 | full_docs_shlv.close()
302 | doc_writer.commit()
303 | return
304 |
305 | #####################################################################
306 | #####################################################################
307 | def get_tags_information(path_to_datadumps, site_name):
308 | """ Get the tags of a single site, and their count.
309 | Return a list of the form: [(tag name, tag count)..]
310 | """
311 | tags_info = []
312 | tags_root = ET.parse(path_to_datadumps + site_name + '/Tags.xml').getroot()
313 | for tag in tags_root:
314 | tag_name = tag.attrib['TagName']
315 | count = tag.attrib['Count']
316 | tags_info.append((tag_name, count))
317 |
318 | return tags_info
319 |
320 | #####################################################################
321 | #####################################################################
322 | #####################################################################
323 |
324 | def main(is_debug_mode):
325 | """ Iterate over all the avaiable datadumps, index them all and create a metadata file.
326 | in debug_mode, allow the user to select which sites to index.
327 | """
328 | if not os.path.exists('../Index'):
329 | os.mkdir('../Index')
330 | if not os.path.exists('../db'):
331 | os.mkdir('../db')
332 | if not os.path.exists('../temp_db'):
333 | os.mkdir('../temp_db')
334 | if not os.path.exists('../Metadata'):
335 | os.mkdir('../Metadata')
336 | if not os.path.exists('../Data'):
337 | os.mkdir('../Data')
338 |
339 | #Clean old metadata files, if present.
340 | files = os.listdir('../Metadata/')
341 | for file in files:
342 | os.remove('../Metadata/'+file)
343 |
344 | path_to_datadumps = '../Datadumps/'
345 | site_names = os.listdir(path_to_datadumps)
346 | num_of_sites = len(site_names)
347 |
348 | #create a shelv to hold the metadata
349 | metadata_shelve = shelve.open('../Metadata/metadata.db', 'n', protocol = -1, writeback = False)
350 |
351 | j=0
352 | for site_name in site_names:
353 |
354 | j += 1
355 | print ("----> Now Parsing {0} ({1}/{2})<-----".format(site_name, j, num_of_sites))
356 | if is_debug_mode:
357 | #Allow the user to skip indexing of a datadump
358 | user_input = raw_input('Skip {0}?'.format(site_name))
359 | if user_input=='y':
360 | #The user does not want to index - but if the site's data as already indexed
361 | #we want it to appear in the metadata
362 | if os.path.isfile('../db/'+site_name+'.db'):
363 | #get the tags information
364 | tags_info = get_tags_information(path_to_datadumps, site_name) #[(tag name, size), ..]
365 |
366 | #Store metadata as shelve dict: {db_name, (number of docs, list of tags)}
367 | full_docs_shlv = shelve.open('../db/' + site_name +'.db', 'r', protocol = -1)
368 | metadata_shelve[site_name] = (str(len(full_docs_shlv.keys())), tags_info)
369 | full_docs_shlv.close()
370 |
371 | continue
372 |
373 | #delete the temp_dbs
374 | temp_files = os.listdir('../temp_db/')
375 | for temp_file in temp_files:
376 | os.remove('../temp_db/'+temp_file)
377 | if os.path.isfile('../db/'+site_name+'.db'):
378 | os.remove('../db/'+site_name+'.db')
379 |
380 | #Parse the xmls, and index the documents
381 | parse_xmls(path_to_datadumps + site_name + '/', site_name)
382 | db_docs_ix_pointer = create_schema('../Index', site_name)
383 | index_data(db_docs_ix_pointer, site_name)
384 |
385 | #get the tags information
386 | tags_info = get_tags_information(path_to_datadumps, site_name) #[(tag name, size), ..]
387 |
388 | #Store metadata as shelve dict: {db_name, (number of docs, list of tags)}
389 | full_docs_shlv = shelve.open('../db/' + site_name +'.db', 'r', protocol = -1)
390 | metadata_shelve[site_name] = (str(len(full_docs_shlv.keys())), tags_info)
391 | full_docs_shlv.close()
392 |
393 | metadata_shelve.close()
394 |
395 | import shutil
396 | shutil.rmtree('../temp_db', ignore_errors=True)
397 |
398 |
399 | if __name__ == "__main__":
400 |
401 | try:
402 | debug_selector = sys.argv[1]
403 | except IndexError:
404 | debug_selector = None
405 |
406 | if debug_selector == "debug":
407 | debug_mode = True
408 | else:
409 | debug_mode = False
410 |
411 | main(debug_mode)
412 |
--------------------------------------------------------------------------------
/Sources/SearchEngine.py:
--------------------------------------------------------------------------------
1 | from whoosh.index import open_dir
2 | from whoosh.qparser import QueryParser
3 | import os
4 | import shelve
5 |
6 | """
7 | Internal Stack Exchange - Search Engine
8 | ---------------------------------------
9 | Holds various function the search the database.
10 | """
11 |
12 | ########################################################
13 | def get_all_index_pointers(path_to_index_folder, db_names):
14 | """ Scan the /Index folder, return the pointers to every index found there.
15 | """
16 | index_pointers = {}
17 | for name in db_names:
18 | pointer = open_dir(path_to_index_folder, name+'_index')
19 | index_pointers[name] = pointer
20 |
21 | return index_pointers
22 |
23 | ########################################################
24 | def get_search_results(index_pointer, search_term, page_number, site_name, is_tag):
25 | """ Query the index for the desired search term. The select field
26 | in the schema ("doc_tags" or "doc_texts") is selected by the is_tag parameter.
27 | """
28 |
29 | #This shlv holds all the docs of the selected site.
30 | full_docs_shlv = shelve.open('../db/' + site_name +'.db', 'r', protocol = -1)
31 |
32 | matched_docs_ids = []
33 | with index_pointer.searcher() as searcher:
34 |
35 | # If we are searching for a tag - query the "doc_tags" field,
36 | # else, query the "doc_texts".
37 | if is_tag == "1":
38 | myquery = QueryParser("doc_tags", index_pointer.schema).parse(search_term)
39 | else: #"0"
40 | myquery = QueryParser("doc_texts", index_pointer.schema).parse(search_term)
41 |
42 | results = searcher.search_page(myquery, page_number)
43 |
44 | #Find the questions Id of the results.
45 | for hit in results:
46 | matched_docs_ids.append(hit['question_id'])
47 |
48 | search_results = []
49 | #Store the title and body of the question, to display to the user.
50 | for doc_id in matched_docs_ids:
51 | tmp_title = full_docs_shlv[str(doc_id)]['Title']
52 | tmp_text = full_docs_shlv[str(doc_id)]['Body']
53 |
54 | search_results.append({'Title':tmp_title, 'Body':tmp_text, 'Id':doc_id})
55 |
56 | full_docs_shlv.close()
57 | return search_results, results.is_last_page()
58 |
59 |
--------------------------------------------------------------------------------
/Sources/WebServer.py:
--------------------------------------------------------------------------------
1 | from bottle import route, run, template, static_file, request
2 | import shelve
3 | import SearchEngine, Indexer
4 | import cPickle
5 | import os, sys
6 |
7 | """
8 | Internal Stack Exchange
9 | -----------------------
10 | A tool to enable using the StackExchange datadumps (https://archive.org/details/stackexchange)
11 | in offline enviroments (e.g. internal network of a company, not connected to the
12 | internet).
13 |
14 | Prequisites:
15 | -Python 2.7
16 | -A linux server with admin priviliges
17 |
18 | Dependencies:
19 | -Bootle
20 | -Whoosh
21 | -CherryPy
22 |
23 | Usage:
24 | -Unzip internal_s_e.zip to some directory.
25 | -Go to the /Sources directory
26 | -type: python Indexer.py
27 | The app will index all the datadumps in the /Datadumps folder.
28 | -type: python Webserver.py
29 | The webserver will start.
30 |
31 | If no IP or Port number were given, the server will launch in 'development mode':
32 | localhost, 8080.
33 |
34 | Open the web browswer, and go to the server's ip address. e.g.:
35 | "http://192.1.2.3:80/"
36 |
37 | TODO:
38 | add related links
39 | add selected answer
40 | add loging and better print out
41 | document in sphinx
42 | add py3 support
43 |
44 |
45 | Changes Tracking:
46 | Ver 1.02 (28.5.15): Fixed a bug that caused the site's docs to be counted wrongly.
47 | Added deletion of temporary db files after each indexing.
48 | Ver 1.03 (3.6.15) : Fixed a bug in sorting of site's tag sizes.
49 | Added the cherrypy server for improved performance
50 | """
51 |
52 | VERSION = 1.03
53 |
54 | ## Webpages ###
55 | ###############
56 |
57 | @route('/')
58 | @route('/index')
59 | def index():
60 | """ Display a list of 'sites'. a 'site' is a single datadump, e.g. 'Beer', 'Math'.
61 | Sites are sorted alpahbeticaly (default) or by size.
62 | """
63 | sort_type = request.query.sort_type or 'name'
64 | sorted_s_e_sites_list = sort_by_name_of_size(s_e_sites, sort_type)
65 |
66 | return template('index', s_e_sites = sorted_s_e_sites_list)
67 |
68 | #####################################
69 | @route('/help')
70 | def help():
71 | return template('help')
72 |
73 | #####################################
74 | @route('/news')
75 | def news():
76 | return template('news')
77 |
78 | #####################################
79 | @route('/about')
80 | def about():
81 | return template('about')
82 |
83 | #####################################
84 | @route('/contact')
85 | def contanct():
86 | return template('contact')
87 |
88 | #####################################
89 | @route('/site')
90 | def site():
91 | """ Display a Search box, and a list of tags avaliable for that site.
92 | Tags can be sorted by name (default) or size.
93 | """
94 | site_name = request.query.site_name
95 | sort_type = request.query.sort_type or 'name'
96 | site_tags = tags_dict[site_name]
97 |
98 | sorted_site_tags = sort_by_name_of_size(site_tags, sort_type)
99 | return template('site', site_name = site_name, site_tags = sorted_site_tags)
100 |
101 | #####################################
102 | @route('/search')
103 | def search():
104 | """ Display the search results, 10 items at a time.
105 | The user can browse to the previous page or the next page.
106 | """
107 |
108 | search_term = request.query.search_term
109 | site_name = request.query.site_name
110 | page_number = int(request.query.page_number or '1', 10)
111 | is_tag = request.query.is_tag #Is the search_term a tag, or is it a free search?
112 |
113 | # If this is the first page, there's no previous page.
114 | if page_number == 1:
115 | prev_page = None
116 | else:
117 | prev_page = page_number-1
118 |
119 | #Load the pointer to the site's search engine index.
120 | index_pointer = index_pointers[site_name]
121 |
122 | search_results , is_last_page = SearchEngine.get_search_results(index_pointer,
123 | search_term,
124 | page_number,
125 | site_name,
126 | is_tag)
127 |
128 | # If this is the last page of results, there's no next page.
129 | if is_last_page:
130 | next_page = None
131 | else:
132 | next_page = page_number+1
133 |
134 | return template('search_results', current_page_num = page_number,
135 | prev_page = prev_page,
136 | next_page = next_page,
137 | site_name = site_name,
138 | is_tag = is_tag,
139 | search_term = search_term,
140 | search_results = search_results)
141 |
142 | #####################################
143 | @route('/display_full_doc')
144 | def display_full_doc():
145 | """ Display a full 'doc': a 'doc' is a question with all of it's answers, comments, etc.
146 | """
147 | doc_id = request.query.doc_id
148 | site_name = request.query.site_name
149 | is_tag = request.query.is_tag
150 | search_term = request.query.search_term
151 | page_number = int(request.query.page_number, 10)
152 |
153 | # Open the database for the selected site, and retrive the doc by it's Id.
154 | full_docs_shlv = shelve.open('../db/' + site_name +'.db', 'r', protocol = -1)
155 | doc_data = full_docs_shlv[str(doc_id)]
156 | full_docs_shlv.close()
157 |
158 | return template('full_doc', search_term = search_term,
159 | site_name = site_name,
160 | page_number = page_number,
161 | is_tag = is_tag,
162 | doc_data = doc_data)
163 |
164 | ###################################################
165 | @route('/static/')
166 | def server_static(filename):
167 | """ Get all the static files: css, images, fonts, etc.
168 | """
169 | return static_file(filename, root = 'static')
170 |
171 | ###################################################
172 | def sort_by_name_of_size(list_to_be_sorted, sort_type):
173 | """ Get a list of tuples such as: [(site name, size), (site name, size)]
174 | and return it sorted by name or size.
175 | """
176 | sorted_list = []
177 | if sort_type == 'name':
178 | sorted_list = sorted(list_to_be_sorted, key = lambda x: x[0], reverse = False)
179 | else: # 'size'
180 | sorted_list = sorted(list_to_be_sorted, key = lambda x: int(x[1],10), reverse = True)
181 |
182 | return sorted_list
183 |
184 | if __name__ == '__main__':
185 |
186 | print ("Starting Internal Stack Exchange, Version {0}. Created by Ran Levi, 2015".format(VERSION))
187 |
188 | # Chech if the user provided an IP and Port number. If not,
189 | # use default values.
190 | try:
191 | ip = sys.argv[1]
192 | port = sys.argv[2]
193 | print "Starting in production Mode: {0}, {1}.".format(ip, port)
194 | except IndexError:
195 | print "Starting in development Mode: localhost, 8080."
196 | ip = 'localhost'
197 | port = 8080
198 |
199 | # The metadata_shelve holds information about available sites and their sizes,
200 | # and the tags for each site, and their sizes.
201 | # {site_name: (sites metadata, tags metadata)}
202 | metadata_shelve = shelve.open('../Metadata/metadata.db', protocol = -1)
203 |
204 | s_e_sites = []
205 | tags_dict = {}
206 | site_names = metadata_shelve.keys()
207 |
208 | for site_name in site_names:
209 | s_e_sites.append((site_name, metadata_shelve[site_name][0]))
210 | tags_dict.update({site_name: metadata_shelve[site_name][1]})
211 |
212 | metadata_shelve.close()
213 |
214 | #Scan the /Index directory for all the available search engine indexes.
215 | index_pointers = SearchEngine.get_all_index_pointers('../Index', site_names)
216 |
217 | #Run the webserver
218 | run(host = ip, port = port, debug = True, server = "cherrypy")
219 |
--------------------------------------------------------------------------------
/Sources/static/fonts/OpenSans-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/OpenSans-Regular.eot
--------------------------------------------------------------------------------
/Sources/static/fonts/OpenSans-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/OpenSans-Regular.otf
--------------------------------------------------------------------------------
/Sources/static/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/Sources/static/fonts/OpenSans-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/OpenSans-Regular.woff
--------------------------------------------------------------------------------
/Sources/static/fonts/Play-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/Play-Regular.eot
--------------------------------------------------------------------------------
/Sources/static/fonts/Play-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/Play-Regular.otf
--------------------------------------------------------------------------------
/Sources/static/fonts/Play-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/Play-Regular.ttf
--------------------------------------------------------------------------------
/Sources/static/fonts/Play-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/fonts/Play-Regular.woff
--------------------------------------------------------------------------------
/Sources/static/images/bg-adbox.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/bg-adbox.jpg
--------------------------------------------------------------------------------
/Sources/static/images/bg-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/bg-button.png
--------------------------------------------------------------------------------
/Sources/static/images/box-of-icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/box-of-icons.png
--------------------------------------------------------------------------------
/Sources/static/images/box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/box.png
--------------------------------------------------------------------------------
/Sources/static/images/divider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/divider.png
--------------------------------------------------------------------------------
/Sources/static/images/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/icons.png
--------------------------------------------------------------------------------
/Sources/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/logo.png
--------------------------------------------------------------------------------
/Sources/static/images/recycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ranlevi/InternalSE/91ec69ba189f7b89965ba103c641b09f05b1ffc2/Sources/static/images/recycle.png
--------------------------------------------------------------------------------
/Sources/static/style.css:
--------------------------------------------------------------------------------
1 | /* Website template by freewebsitetemplates.com */
2 | @font-face {
3 | /*font-family: 'OpenSans';*/
4 | font-family: 'Verdana';
5 | src: url('../static/fonts/OpenSans-Regular.eot');
6 | src: local('☺'), url('../static/fonts/OpenSans-Regular.woff') format('woff'), url('../static/fonts/OpenSans-Regular.ttf') format('truetype'), url('../static/fonts/OpenSans-Regular.svg') format('svg');
7 | font-weight: normal;
8 | font-style: normal;
9 | }
10 | @font-face {
11 | font-family: 'Play';
12 | src: url('../static/fonts/Play-Regular.eot');
13 | src: local('☺'), url('../static/fonts/Play-Regular.woff') format('woff'), url('../static/fonts/Play-Regular.ttf') format('truetype'), url('../static/fonts/Play-Regular.svg') format('svg');
14 | font-weight: normal;
15 | font-style: normal;
16 | }
17 | body {
18 | background-color: #fff;
19 | font-family: 'OpenSans';
20 | margin: 0;
21 | }
22 | img {
23 | border: 0;
24 | }
25 | .clearfix:after, #contents:after {
26 | clear:both;
27 | content:"";
28 | display:block;
29 | height:1%;
30 | line-height:0;
31 | visibility:hidden;
32 | }
33 | .btn {
34 | background: url(../static/images/bg-button.png) no-repeat;
35 | background-position: 0 -70px;
36 | color: #fff;
37 | display: inline-block;
38 | font: 24px/60px 'OpenSans';
39 | height: 60px;
40 | width: 230px;
41 | text-align: center;
42 | text-decoration: none;
43 | text-transform: uppercase;
44 | }
45 | .btn:hover {
46 | background-position: 0 0;
47 | }
48 | /*------------------------------ HEADER ------------------------------*/
49 | #header {
50 | background-color: #eee;
51 | border-bottom: 1px solid #e6e6e6;
52 | padding: 27px 0;
53 | }
54 | #header > div, #footer > div {
55 | width: 920px;
56 | margin: 0 auto;
57 | padding: 0 20px;
58 | }
59 | /** Logo **/
60 | #header .logo {
61 | float: left;
62 | margin-right: 20px;
63 | }
64 | /**
65 | #header .logo a {
66 | background: url(../static/images/logo.png) no-repeat center top;
67 | color: #000;
68 | display: block;
69 | font: 15px/30px 'Play';
70 | height: 20px;
71 | width: 76px;
72 | padding-top: 68px;
73 | text-decoration: none;
74 | text-transform: uppercase;
75 | }
76 | **/
77 | /** Navigation **/
78 | #navigation {
79 | display: inline-block;
80 | list-style: none;
81 | line-height: 100px;
82 | margin: 0;
83 | padding: 0;
84 | }
85 | #navigation ul {
86 | display: inline-block;
87 | list-style: none;
88 | margin: 0;
89 | padding: 0;
90 | }
91 | #navigation li {
92 | float: left;
93 | width: 160px;
94 | text-align: center;
95 | }
96 | #navigation li a {
97 | color: #818181;
98 | font-size: 15px;
99 | line-height: 30px;
100 | text-decoration: none;
101 | }
102 | #navigation li a:hover {
103 | color: #000;
104 | }
105 | #navigation li.active a {
106 | color: #f99600;
107 | }
108 | /*------------------------------ CONTENTS ------------------------------*/
109 | #contents {
110 | min-height: 510px;
111 | width: 880px;
112 | margin: 0 auto;
113 | padding: 54px 40px;
114 | }
115 | h1 {
116 | color: #3e3e3e;
117 | font-size: 30px;
118 | font-weight: normal;
119 | line-height: 30px;
120 | margin: 0 0 30px;
121 | }
122 | h2 {
123 | /*color: #2c2c2c;*/
124 | color: maroon;
125 | font-size: 24px;
126 | font-weight: normal;
127 | line-height: 24px;
128 | margin: 0 0 12px;
129 | }
130 | p {
131 | /*color: #585858;*/
132 | color: black;
133 | font-size: 16px;
134 | line-height: 24px;
135 | margin: 0 0 30px;
136 | }
137 | p a {
138 | color: #585858;
139 | }
140 | #tagline h1 {
141 | color: maroon;
142 | margin-left: 20px;
143 | }
144 | #tagline > div {
145 | float: left;
146 | width: 800px;
147 | margin: 0 20px;
148 | }
149 | #contents .features {
150 | width: 810px;
151 | margin: 0 auto;
152 | }
153 | .features > div {
154 | display: inline-block;
155 | margin: 0 0 30px;
156 | }
157 | .features > div img {
158 | float: left;
159 | margin-right: 20px;
160 | margin-top: 36px;
161 | }
162 | .date {
163 | float: left;
164 | height: 78px;
165 | width: 70px;
166 | margin-right: 20px;
167 | border: 1px solid #d5d5d5;
168 | text-align: center;
169 | }
170 |
171 | #comments {
172 | font-size: 13px;
173 | margin-bottom: 6px;
174 | }
175 | .date p {
176 | margin: 12px 0 0;
177 | }
178 | .date p span {
179 | display: block;
180 | font-size: 30px;
181 | margin-bottom: 6px;
182 | }
183 | .author {
184 | color: #585858;
185 | display: block;
186 | font-size: 12px;
187 | }
188 | .more {
189 | background-color: #727272;
190 | color: #fff;
191 | display: inline-block;
192 | font-size: 14px;
193 | line-height: 30px;
194 | width: 100px;
195 | text-align: center;
196 | text-decoration: none;
197 | }
198 | .more:hover, .message input[type='submit']:hover {
199 | background-color: #f99600;
200 | color: #000;
201 | }
202 | /** main **/
203 | .main {
204 | float: left;
205 | background: url(../static/images/divider.png) repeat-y right top;
206 | min-height: 100px;
207 | width: 620px;
208 | padding-right: 24px;
209 | }
210 | .main h1, .sidebar h1 {
211 | margin: 0 0 12px;
212 | position: relative;
213 | top: -18px;
214 | }
215 | .main h2 span {
216 | display: block;
217 | font-size: 12px;
218 | }
219 | /** sidebar **/
220 | .sidebar {
221 | float: left;
222 | min-height: 848px;
223 | width: 216px;
224 | margin-left: 20px;
225 | }
226 | .sidebar ul, .news {
227 | list-style: none;
228 | margin: 0;
229 | padding: 0;
230 | }
231 | .news li {
232 | border-top: 1px solid #d5d5d5;
233 | padding: 24px 30px 0 100px;
234 | position: relative;
235 | }
236 | .news li .date {
237 | float: none;
238 | position: absolute;
239 | left: 0;
240 | top: 30px;
241 | }
242 | .news li > p span, .post > span {
243 | display: block;
244 | text-align: right;
245 | }
246 | .posts {
247 | border-top: 1px solid #d5d5d5;
248 | }
249 | .posts li {
250 | border-bottom: 1px solid #d5d5d5;
251 | padding: 24px 10px 0;
252 | }
253 | .posts li p {
254 | font-size: 14px;
255 | }
256 | .posts li .title {
257 | font-size: 16px;
258 | font-weight: normal;
259 | margin: 0 0 12px;
260 | }
261 | .posts li .title a {
262 | color: #2c2c2c;
263 | font-size: 16px;
264 | text-decoration: none;
265 | }
266 | .post {
267 | width: 785px;
268 | margin: 0 auto;
269 | }
270 | .post h1 {
271 | padding-top: 12px;
272 | }
273 | #about {
274 | width: 740px;
275 | margin: 0 auto;
276 | }
277 | #about h1, .section h1 {
278 | border-bottom: 1px solid #e0e0e0;
279 | padding-bottom: 12px;
280 | }
281 | .section {
282 | float: left;
283 | width: 390px;
284 | margin-right: 50px;
285 | }
286 | .section h1 {
287 | margin-bottom: 18px;
288 | }
289 | .message input[type='text'], .message textarea {
290 | color: #aeaeae;
291 | font-size: 13px;
292 | height: 33px;
293 | line-height: 33px;
294 | width: 380px;
295 | border: 1px solid #d5d5d5;
296 | margin: 0 0 6px;
297 | padding: 0 4px;
298 | }
299 | .message textarea {
300 | height: 175px;
301 | overflow: auto;
302 | resize: none;
303 | }
304 | .message input[type='submit'] {
305 | float: right;
306 | background-color: #818181;
307 | color: #d5d5d5;
308 | cursor: pointer;
309 | font: 13px/30px Arial, Helvetica, sans-serif;
310 | height: 30px;
311 | border: 0;
312 | margin: 0;
313 | padding: 0 10px;
314 | }
315 | .contact {
316 | background-color: #f8f8f8;
317 | width: 270px;
318 | padding: 124px 60px;
319 | text-align: center;
320 | }
321 | .contact p span {
322 | color: #2c2c2c;
323 | display: block;
324 | font-size: 30px;
325 | line-height: 36px;
326 | padding: 18px 0;
327 | }
328 | /*------------------------------ FOOTER ------------------------------*/
329 | #footer {
330 | background-color: #eee;
331 | border-top: 1px solid #d8d8d8;
332 | padding: 30px 0;
333 | }
334 | #footer p {
335 | font-size: 12px;
336 | line-height: 30px;
337 | padding-left: 10px;
338 | }
339 |
--------------------------------------------------------------------------------
/Sources/views/about.tpl:
--------------------------------------------------------------------------------
1 | %include('header.tpl')
2 |
3 |
4 |
About
5 |
6 |
7 | Intranet StackExchange: Created by Ran Levi, 2015, www.ranlevi.com
8 |