├── 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 |

9 |
10 |
11 |
12 | 13 | %include('footer.tpl') 14 | -------------------------------------------------------------------------------- /Sources/views/contact.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 |

Contact

5 |
6 |

7 |

8 |
9 |
10 |
11 | 12 | %include('footer.tpl') 13 | -------------------------------------------------------------------------------- /Sources/views/footer.tpl: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/views/full_doc.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 | Back to Site Page
5 | Back to Search Results
6 |

7 |

{{!doc_data['Title']}}

8 |
9 | {{!doc_data['Body']}} 10 |

11 | Name: {{!doc_data['User']['DisplayName']}} 12 | Reputation: {{!doc_data['User']['Reputation']}} 13 | DownVotes: {{!doc_data['User']['DownVotes']}} 14 | UpVotes: {{!doc_data['User']['UpVotes']}} 15 |

16 |

17 | Score: {{!doc_data['Score']}} 18 | Related Links: 19 | %for link in doc_data['PostLinks']: 20 | Link, 21 | %end 22 | 23 |

24 |

25 | Comments: 26 |

    27 | %for comment in doc_data['Comments']: 28 |
  • @{{!comment['User']['DisplayName']}}: {{!comment['Text']}}
  • 29 | %end 30 |
31 |

32 |

Answers:

33 |

34 | %for answer in doc_data['Answers']: 35 |


36 | {{!answer['Body']}} 37 |

38 |

39 | Name: {{!answer['User']['DisplayName']}} 40 | Reputation: {{!answer['User']['Reputation']}} 41 | DownVotes: {{!answer['User']['DownVotes']}} 42 | UpVotes: {{!answer['User']['UpVotes']}} 43 |

44 |

45 | Score: {{!answer['Score']}} 46 | Related Links: 47 | %for link in answer['PostLinks']: 48 | Link, 49 | %end 50 |

51 |

52 | Comments: 53 |

    54 | %for comment in answer['Comments']: 55 |
  • @{{!comment['User']['DisplayName']}}: {{!comment['Text']}}
  • 56 | %end 57 |
58 |

59 | % end 60 |
61 |

62 |
63 |
64 | 65 | %include('footer.tpl') 66 | -------------------------------------------------------------------------------- /Sources/views/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Internal Stack Exchange 7 | 8 | 9 | 10 | 34 | 35 | -------------------------------------------------------------------------------- /Sources/views/help.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 |

Help

5 |
6 |

7 |

8 |
9 |
10 |
11 | 12 | %include('footer.tpl') 13 | -------------------------------------------------------------------------------- /Sources/views/index.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 |

Welcome. Select a Stack Exchange site:

5 |
6 |

7 | (Sort by Name/Size) 8 |

    9 | % for site in s_e_sites: 10 |
  • {{site[0]}} ({{site[1]}})
  • 11 | % end 12 |
13 |

14 |
15 |
16 |
17 | 18 | %include('footer.tpl') 19 | -------------------------------------------------------------------------------- /Sources/views/news.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 |

News

5 |
6 |

7 |

8 |
9 |
10 |
11 | 12 | %include('footer.tpl') 13 | -------------------------------------------------------------------------------- /Sources/views/search_results.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 |

Back to Site Page 5 |

Results for: {{search_term}}

6 |
7 |

8 | %if prev_page != None: 9 | Previous 10 Results 10 | %end 11 | %if next_page != None: 12 | Next 10 results

13 | %end 14 |

15 |

16 |

    17 | %for result in search_results: 18 |
  • {{!result['Title']}}

    19 | {{!result['Body']}} 20 | More 21 |
  • 22 | %end 23 |
24 |

25 |
26 |
27 |
28 | 29 | %include('footer.tpl') 30 | -------------------------------------------------------------------------------- /Sources/views/site.tpl: -------------------------------------------------------------------------------- 1 | %include('header.tpl') 2 |
3 |
4 |

Back to Sites List 5 |

{{site_name}}

6 |
7 |
8 | Free Search:
9 | 10 | 11 | 12 | 13 | 14 |
15 |

Tags:


16 | (Sort by Name/ 17 | Size) 18 |
    19 | % for tag in site_tags: 20 |
  • {{tag[0]}} ({{tag[1]}})
  • 21 | % end 22 |
23 |

24 |
25 |
26 |
27 | 28 | %include('footer.tpl') 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | CherryPy==3.7.0 2 | Whoosh==2.7.0 3 | bottle==0.12.8 4 | --------------------------------------------------------------------------------