├── buzzengine ├── api │ ├── __init__.py │ ├── urls.py │ ├── tasks.py │ ├── middleware.py │ ├── forms.py │ ├── models.py │ └── views.py ├── __init__.py ├── frontend │ ├── __init__.py │ ├── urls.py │ ├── forms.py │ ├── decorators.py │ └── views.py ├── static │ ├── images │ │ ├── favicon.ico │ │ ├── ajax-loader.gif │ │ └── fork-github.png │ ├── js │ │ ├── flXHR │ │ │ ├── flXHR.swf │ │ │ ├── updateplayer.swf │ │ │ ├── flXHR.vbs │ │ │ ├── flensed.js │ │ │ ├── checkplayer.js │ │ │ ├── swfobject.js │ │ │ └── flXHR.js │ │ ├── buzzengine.min.js │ │ ├── buzzengine.js │ │ └── jquery-1.5.1.min.js │ ├── load-policy.xml │ └── css │ │ ├── comments.css │ │ └── syntax.css ├── templates │ ├── frontend │ │ ├── admin │ │ │ ├── delete_done.html │ │ │ ├── edit_done.html │ │ │ ├── login_required.html │ │ │ ├── delete.html │ │ │ ├── edit.html │ │ │ └── layout.html │ │ └── homepage.html │ └── api │ │ ├── email_notification.txt │ │ ├── test_page.html │ │ └── comments.html ├── urls.py ├── email_handler.py ├── settings.py └── handler.py ├── .gitignore ├── app.yaml ├── LICENSE └── README.md /buzzengine/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /buzzengine/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /buzzengine/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | .idea 4 | buzzengine.iml 5 | -------------------------------------------------------------------------------- /buzzengine/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandru/OldBuzzEngine/HEAD/buzzengine/static/images/favicon.ico -------------------------------------------------------------------------------- /buzzengine/static/js/flXHR/flXHR.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandru/OldBuzzEngine/HEAD/buzzengine/static/js/flXHR/flXHR.swf -------------------------------------------------------------------------------- /buzzengine/static/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandru/OldBuzzEngine/HEAD/buzzengine/static/images/ajax-loader.gif -------------------------------------------------------------------------------- /buzzengine/static/images/fork-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandru/OldBuzzEngine/HEAD/buzzengine/static/images/fork-github.png -------------------------------------------------------------------------------- /buzzengine/static/js/flXHR/updateplayer.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandru/OldBuzzEngine/HEAD/buzzengine/static/js/flXHR/updateplayer.swf -------------------------------------------------------------------------------- /buzzengine/templates/frontend/admin/delete_done.html: -------------------------------------------------------------------------------- 1 | {% extends "frontend/admin/layout.html" %} 2 | {% block content %} 3 | 4 |

Deleted article ...

5 | 6 |

7 | View comments 8 |

9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /buzzengine/static/load-policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /buzzengine/templates/frontend/admin/edit_done.html: -------------------------------------------------------------------------------- 1 | {% extends "frontend/admin/layout.html" %} 2 | {% block content %} 3 | 4 |

Edit succesful

5 | 6 |

7 | Back to editing form 8 |

9 |

10 | View comment 11 |

12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /buzzengine/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | from django.conf.urls.defaults import * 9 | 10 | urlpatterns = patterns('', 11 | (r'^api/', include('buzzengine.api.urls')), 12 | (r'', include('buzzengine.frontend.urls')), 13 | ) 14 | 15 | 16 | -------------------------------------------------------------------------------- /buzzengine/frontend/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | from django.conf.urls.defaults import * 9 | from buzzengine.frontend import views 10 | 11 | 12 | urlpatterns = patterns('', 13 | (r'^$', views.homepage), 14 | (r'^admin/edit/$', views.edit_comment), 15 | ) 16 | -------------------------------------------------------------------------------- /buzzengine/templates/frontend/admin/login_required.html: -------------------------------------------------------------------------------- 1 | {% extends "frontend/admin/layout.html" %} 2 | 3 | {% block title %}403 Forbidden{% endblock %} 4 | {% block topbar %}{% endblock %} 5 | 6 | {% block content %} 7 | 8 |

9 | You do not have permission to access this URL. 10 |

11 |

12 | Logged in with: {{ user.email }} 13 |

14 |

15 | Log with another account 16 |

17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /buzzengine/frontend/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "alex@magnolialabs.com" 6 | 7 | 8 | from google.appengine.ext.db import djangoforms as forms 9 | #from django import newforms as forms 10 | from buzzengine.api.models import Comment 11 | 12 | class CommentForm(forms.ModelForm): 13 | class Meta: 14 | model = Comment 15 | exclude = ['author_ip', 'article', 'author'] -------------------------------------------------------------------------------- /buzzengine/email_handler.py: -------------------------------------------------------------------------------- 1 | import logging, email 2 | from google.appengine.ext import webapp 3 | from google.appengine.ext.webapp.mail_handlers import InboundMailHandler 4 | from google.appengine.ext.webapp.util import run_wsgi_app 5 | 6 | class LogSenderHandler(InboundMailHandler): 7 | def receive(self, mail_message): 8 | logging.info("Received a message from: " + mail_message.sender) 9 | 10 | application = webapp.WSGIApplication([LogSenderHandler.mapping()], debug=True) 11 | -------------------------------------------------------------------------------- /buzzengine/templates/api/email_notification.txt: -------------------------------------------------------------------------------- 1 | {{ author.name }} sent you a message on: 2 | {{ article_url }} 3 | 4 | {{ comment.comment }} 5 | 6 | Author info: 7 | 8 | - name: {{ author.name }} 9 | - email: {{ author.email }} 10 | - url: {{ author.url }} 11 | - ip: {{ comment.author_ip }} 12 | 13 | To edit or delete comment: 14 | http://{{ API_DOMAIN }}/admin/edit/?article_url={{ article_url|urlencode }}&comment_id={{ comment.key.id|urlencode }} 15 | 16 | -- 17 | TheBuzzEngine -------------------------------------------------------------------------------- /buzzengine/api/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | from django.conf.urls.defaults import * 9 | from buzzengine.api import views 10 | from buzzengine.api import tasks 11 | 12 | urlpatterns = patterns('', 13 | (r'^hello/$', views.say_hello), 14 | (r'^comments/$', views.comments), 15 | (r'^notify/$', tasks.notify), 16 | (r'^test/page.html$', views.test_page), 17 | (r'^export/$', views.export_xml), 18 | ) 19 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: thebuzzengine 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | default_expiration: "30d" 7 | 8 | builtins: 9 | - remote_api: on 10 | - datastore_admin: on 11 | 12 | inbound_services: 13 | - mail 14 | 15 | handlers: 16 | 17 | - url: /crossdomain.xml 18 | static_files: buzzengine/static/load-policy.xml 19 | upload: buzzengine/static/load-policy.xml 20 | 21 | - url: /favicon.ico 22 | static_files: buzzengine/static/images/favicon.ico 23 | upload: buzzengine/static/images/favicon.ico 24 | 25 | - url: /static 26 | static_dir: buzzengine/static 27 | 28 | - url: /_ah/mail/.+ 29 | script: buzzengine/email_handler.py 30 | login: admin 31 | 32 | - url: /.* 33 | script: buzzengine/handler.py 34 | 35 | -------------------------------------------------------------------------------- /buzzengine/templates/frontend/admin/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "frontend/admin/layout.html" %} 2 | {% block content %} 3 | 4 |

Delete Comment

5 | 6 |
7 | 8 |

9 | Article: {{ comment.article.title|default:comment.article.url }} 10 |

11 |

12 | Author: {{ comment.author.name }} {% if comment.author.email %}({{ comment.author.email }}){% endif %} 13 |

14 |

15 | {{ comment.comment|escape|linebreaks }} 16 |

17 |

18 | 19 |

20 |
21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /buzzengine/templates/frontend/admin/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "frontend/admin/layout.html" %} 2 | {% block content %} 3 | 4 |

Edit Comment

5 | 6 |
7 |

8 | Article: {{ comment.article.title|default:comment.article.url }} 9 |

10 |

11 | Author: {{ comment.author.name }} {% if comment.author.email %}({{ comment.author.email }}){% endif %} 12 |

13 |

14 | IP: {{ comment.author_ip }} 15 |

16 |

17 | {{ form.comment }} 18 | {% if form.comment.errors %}{{ form.comment.errors }}{% endif %} 19 |

20 |

21 | 22 | 23 |

24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /buzzengine/frontend/decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | from django.conf import settings 9 | from google.appengine.api import users 10 | from django.http import HttpResponse, HttpResponseRedirect, Http404 11 | from django.shortcuts import render_to_response 12 | 13 | 14 | def requires_admin(view): 15 | def f(request, *args, **kwargs): 16 | user = users.get_current_user() 17 | uri = "http://" + request.API_DOMAIN + request.get_full_path() 18 | 19 | if not user: 20 | return HttpResponseRedirect(users.create_login_url(uri)) 21 | 22 | if not users.is_current_user_admin(): 23 | resp = render_to_response("frontend/admin/login_required.html", {'login_url': users.create_login_url(uri), 'user': user}) 24 | resp.status_code = 403 25 | return resp 26 | 27 | request.user = user 28 | return view(request, *args, **kwargs) 29 | 30 | return f 31 | 32 | -------------------------------------------------------------------------------- /buzzengine/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # when a new comment happens, 4 | # this email address receives an alert 5 | ADMIN_EMAIL = "buzzengine@volatile1612.alexn.org" 6 | 7 | # FROM header of new message notifications. Unfortunately it must be 8 | # an approved sender ... like the emails of admins you approve for the 9 | # GAE Application Instance. 10 | # 11 | # Some details here: 12 | # http://code.google.com/appengine/docs/python/mail/sendingmail.html 13 | # 14 | EMAIL_SENDER = "TheBuzzEngine " 15 | 16 | 17 | ## Web framework specific stuff ... 18 | 19 | DEBUG = False 20 | ROOT_PATH = os.path.dirname(__file__) 21 | 22 | MIDDLEWARE_CLASSES = ( 23 | 'django.middleware.common.CommonMiddleware', 24 | 'buzzengine.api.middleware.TrackingMiddleware', 25 | 'buzzengine.api.middleware.HttpControlMiddleware', 26 | ) 27 | INSTALLED_APPS = ( 28 | 'buzzengine.api', 29 | 'buzzengine.frontend', 30 | ) 31 | TEMPLATE_DIRS = ( 32 | os.path.join(ROOT_PATH, 'templates'), 33 | ) 34 | ROOT_URLCONF = 'buzzengine.urls' 35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Alexandru Nedelcu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /buzzengine/handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | import logging 9 | import os,sys 10 | os.environ['DJANGO_SETTINGS_MODULE'] = 'buzzengine.settings' 11 | 12 | # Google App Engine imports. 13 | from google.appengine.ext.webapp import util 14 | 15 | # Force Django to reload its settings. 16 | from django.conf import settings 17 | settings._target = None 18 | 19 | import django.core.handlers.wsgi 20 | import django.core.signals 21 | import django.db 22 | import django.dispatch.dispatcher 23 | 24 | def log_exception(*args, **kwds): 25 | logging.exception('Exception in request:') 26 | 27 | # Log errors. 28 | django.dispatch.dispatcher.connect( 29 | log_exception, django.core.signals.got_request_exception) 30 | 31 | # Unregister the rollback event handler. 32 | django.dispatch.dispatcher.disconnect( 33 | django.db._rollback_on_exception, 34 | django.core.signals.got_request_exception) 35 | 36 | def main(): 37 | # Create a Django application for WSGI. 38 | application = django.core.handlers.wsgi.WSGIHandler() 39 | 40 | # Run the WSGI CGI handler with that application. 41 | util.run_wsgi_app(application) 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /buzzengine/api/tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | from google.appengine.api import mail 9 | from django.template.loader import get_template 10 | from django.template import Context 11 | from django.http import HttpResponse, Http404 12 | from django.conf import settings 13 | from buzzengine.api import models 14 | 15 | 16 | def notify(request): 17 | comment_id = request.REQUEST.get('comment_id') 18 | article_url = request.REQUEST.get('article_url') 19 | 20 | article = models.Article.get_by_key_name(article_url) 21 | if not article: raise Http404 22 | 23 | comment_id = int(comment_id) 24 | comment = models.Comment.get_by_id(comment_id, parent=article) 25 | if not comment: raise Http404 26 | 27 | author = comment.author 28 | 29 | tpl = get_template("api/email_notification.txt") 30 | ctx = Context({'author': author, 'article_url': article_url, 'comment': comment, 'API_DOMAIN': request.API_DOMAIN}) 31 | txt = tpl.render(ctx) 32 | 33 | mail.send_mail( 34 | sender=settings.EMAIL_SENDER, 35 | to=settings.ADMIN_EMAIL, 36 | subject=author.name + " commented on your blog", 37 | body=txt) 38 | 39 | return HttpResponse("Mail sent!") 40 | -------------------------------------------------------------------------------- /buzzengine/static/js/flXHR/flXHR.vbs: -------------------------------------------------------------------------------- 1 | ' flXHR 1.0.6 (VB Binary Helpers) 2 | ' Copyright (c) 2008-2010 Kyle Simpson, Getify Solutions, Inc. 3 | ' This software is released under the MIT License 4 | ' 5 | ' ==================================================================================================== 6 | 7 | Function flXHR_vb_BinaryToString(obj) 8 | Dim I,S 9 | Dim J 10 | For I = 1 to LenB(obj) 11 | J = AscB(MidB(obj,I,1)) 12 | If J = 0 Then 13 | S = S & "" 14 | Else 15 | S = S & Chr(J) 16 | End If 17 | Next 18 | flXHR_vb_BinaryToString = S 19 | End Function 20 | 21 | Function flXHR_vb_SizeOfBytes(obj) 22 | Dim I 23 | I = LenB(obj) 24 | flXHR_vb_SizeOfBytes = I 25 | End Function 26 | 27 | Function flXHR_vb_StringToBinary(str) 28 | dim binobj 29 | dim ahex(),oparser,oelem 30 | redim ahex(len(str)-1) 31 | for i=0 to len(str)-1 32 | ahex(i)=right("00" & hex(asc(mid(str,i+1,1))),2) 33 | next 34 | set oparser=createobject("msxml2.domdocument") 35 | with oparser 36 | set oelem=.createElement("x") 37 | oelem.datatype="bin.hex" 38 | oelem.text=join(ahex,"") 39 | binobj=oelem.nodetypedvalue 40 | end with 41 | set oelem=nothing 42 | set oparser=nothing 43 | flXHR_vb_StringToBinary=binobj 44 | End Function 45 | -------------------------------------------------------------------------------- /buzzengine/templates/api/test_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TheBuzzEngine - Test Page 6 | 7 | 8 |
9 | Lorem Ipsum is simply dummy text of the printing and typesetting 10 | industry. Lorem Ipsum has been the industry's standard dummy 11 | text ever since the 1500s, when an unknown printer took a galley 12 | of type and scrambled it to make a type specimen book. It has 13 | survived not only five centuries, but also the leap into 14 | electronic typesetting, remaining essentially unchanged. It was 15 | popularised in the 1960s with the release of Letraset sheets 16 | containing Lorem Ipsum passages, and more recently with desktop 17 | publishing software like Aldus PageMaker including versions of 18 | Lorem Ipsum. 19 |
20 | 21 |
22 | 38 | 39 | -------------------------------------------------------------------------------- /buzzengine/frontend/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | import hashlib 9 | import re 10 | 11 | from urllib import quote 12 | from datetime import datetime, timedelta 13 | from django.utils import simplejson as json 14 | 15 | from google.appengine.api import users 16 | from django.shortcuts import render_to_response 17 | from django.http import HttpResponse, HttpResponseRedirect, Http404 18 | from django.template import RequestContext 19 | from buzzengine.api import models 20 | from buzzengine.frontend.decorators import requires_admin 21 | from buzzengine.frontend.forms import CommentForm 22 | 23 | 24 | def homepage(request): 25 | return render_to_response("frontend/homepage.html", {'API_DOMAIN': request.API_DOMAIN}) 26 | 27 | 28 | @requires_admin 29 | def edit_comment(request): 30 | comment = _get_item(request) 31 | form = CommentForm(instance=comment) 32 | 33 | if request.method == "POST": 34 | if request.POST.get('delete'): 35 | comment.delete() 36 | 37 | article_url = request.REQUEST.get('article_url') 38 | if article_url.find('#') == -1: 39 | article_url += '#comments' 40 | 41 | return HttpResponseRedirect(article_url) 42 | else: 43 | form = CommentForm(request.POST, instance=comment) 44 | if form.is_valid(): 45 | form.save() 46 | return _render(request, "frontend/admin/edit.html", {"form": form, "comment": comment, 'message': 'Message saved!'}) 47 | 48 | return _render(request, "frontend/admin/edit.html", {"form": form, "comment": comment}) 49 | 50 | 51 | def _get_item(request): 52 | comment_id = request.REQUEST.get('comment_id') 53 | article_url = request.REQUEST.get('article_url') 54 | 55 | article = models.Article.get_by_key_name(article_url) 56 | if not article: raise Http404 57 | 58 | comment_id = int(comment_id) 59 | comment = models.Comment.get_by_id(comment_id, parent=article) 60 | if not comment: raise Http404 61 | 62 | return comment 63 | 64 | 65 | def _render(request, tpl_name, kwargs): 66 | kwargs['user'] = request.user 67 | kwargs['logout_url'] = users.create_logout_url("/") 68 | return render_to_response(tpl_name, kwargs) -------------------------------------------------------------------------------- /buzzengine/api/middleware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "alex@magnolialabs.com" 6 | 7 | import re 8 | from django.conf import settings 9 | from buzzengine.api.models import Author 10 | 11 | 12 | class TrackingMiddleware: 13 | def process_request(self, request): 14 | authorhash = request.GET.get('author') or request.COOKIES.get('author') 15 | if authorhash: 16 | request.author = Author.get_by_hash(authorhash) 17 | else: 18 | request.author = None 19 | 20 | 21 | class HttpControlMiddleware(object): 22 | 23 | def _do_process_request(self, request): 24 | if hasattr(request, 'ROOT_DOMAIN') and hasattr(request, 'API_DOMAIN'): 25 | return 26 | 27 | url = request.REQUEST.get('article_url') or request.META.get('HTTP_REFERER') 28 | rorigin = request.REQUEST.get('origin') 29 | host = None 30 | origin = None 31 | 32 | if url: 33 | origin = re.findall('^(https?://[^/]+)', url) 34 | origin = origin[0] if origin else None 35 | elif rorigin and re.match('^https?://[^/]+$', rorigin): 36 | origin = rorigin 37 | 38 | host = request.META['SERVER_NAME'] 39 | if str(request.META.get('SERVER_PORT')) != str(80): 40 | host += ":" + request.META['SERVER_PORT'] 41 | 42 | request.API_DOMAIN = host 43 | request.ROOT_DOMAIN = origin 44 | 45 | 46 | def process_request(self, request): 47 | self._do_process_request(request) 48 | 49 | 50 | def process_response(self, request, response): 51 | # for some weird reason, ROOT_DOMAIN sometimes is not set, 52 | # although process_request should always be called before 53 | # process_response 54 | 55 | self._do_process_request(request) 56 | origin = request.ROOT_DOMAIN if hasattr(request, 'ROOT_DOMAIN') else None 57 | 58 | if origin: 59 | response['Access-Control-Allow-Origin'] = origin or "*" 60 | response['Access-Control-Allow-Credentials'] = 'true' 61 | response['Access-Control-Allow-Headers'] = 'Content-Type, *' 62 | response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' 63 | response['Access-Control-Max-Age'] = '111111' 64 | 65 | return response 66 | -------------------------------------------------------------------------------- /buzzengine/templates/api/comments.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | {% for obj in comments %} 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | {% if obj.author.url %}{% endif %} 14 | {{ obj.author.name|escape }} 15 | {% if obj.author.url %}{% endif %} 16 | 17 | on 18 | {{ obj.created_at|date }} 19 |
20 |
{{ obj.comment|escape|linebreaks }}
21 |
22 |
23 |
24 | {% endfor %} 25 | 26 |
27 |
28 | {% if form.errors %} 29 |
Cannot send comment. Please correct the errors below!
30 | {% endif %} 31 | 32 | {{ form.article_url }} 33 | {{ form.article_title }} 34 | 35 |
36 | 37 | {{ form.author_name }} 38 | 39 | {% if form.author_name.errors %} 40 | {{ form.author_name.errors }} 41 | {% endif %} 42 |
43 | 44 |
45 | 46 | {{ form.author_email }} 47 | Your email is safe, it won't get published 48 | 49 | {% if form.author_email.errors %} 50 | {{ form.author_email.errors }} 51 | {% endif %} 52 |
53 | 54 |
55 | 56 | {{ form.author_url }} 57 | 58 | {% if form.author_url.errors %} 59 | {{ form.author_url.errors }} 60 | {% endif %} 61 |
62 | 63 |
64 | 65 | {{ form.comment }} 66 | No HTML allowed, just plain text. 67 | 68 | {% if form.comment.errors %} 69 | {{ form.comment.errors }} 70 | {% endif %} 71 |
72 | 73 | 74 | 75 |
76 |
-------------------------------------------------------------------------------- /buzzengine/static/css/comments.css: -------------------------------------------------------------------------------- 1 | #comments { 2 | margin: 0; 3 | padding: 0; 4 | margin-top: 30px; 5 | 6 | font-family: Helvetica, Verdana, sans; 7 | font-size: 14px; 8 | } 9 | #comments .odd { 10 | background-color: #F0F0F0; 11 | } 12 | #comments .comment-item { 13 | padding: 10px; 14 | clear: both; 15 | } 16 | #comments .comment-item img { 17 | display: block; 18 | float: left; 19 | margin-right: 10px; 20 | } 21 | #comments .comment-item .author { 22 | font-size: 12px; 23 | font-weight: bold; 24 | margin-bottom: 5px; 25 | color: #333; 26 | } 27 | #comments .comment-item .author a { 28 | text-decoration: none; 29 | color: #2962A7; 30 | } 31 | #comments .comment-item .author a:hover { 32 | text-decoration: underline; 33 | } 34 | #comments .comment-item .content { 35 | min-height: 80px; 36 | max-width: 500px; 37 | float: left; 38 | } 39 | #comments .comment-item:after { 40 | display: block; 41 | content: "."; 42 | clear: both; 43 | height: 0; 44 | visibility: hidden; 45 | } 46 | #comments form { 47 | clear: both; 48 | margin-top: 20px; 49 | margin-left: 10px; 50 | } 51 | #comments form label { 52 | float: left; 53 | display: block; 54 | width: 85px; 55 | 56 | margin-top: 10px; 57 | color: #333; 58 | } 59 | #comments form input { 60 | border: 1px solid black; 61 | outline: 0; 62 | height: 20px; 63 | width: 296px; 64 | 65 | margin-top: 10px; 66 | clear: right; 67 | padding: 2px; 68 | } 69 | #comments form .help { 70 | display: block; 71 | margin-left: 90px; 72 | color: #666; 73 | font-size: 12px; 74 | } 75 | #comments form textarea { 76 | border: 1px solid black; 77 | width: 290px; 78 | height: 80px; 79 | outline: 0; 80 | margin-top: 10px; 81 | clear: right; 82 | padding: 5px; 83 | } 84 | #comments form .required label { 85 | font-weight: bold; 86 | } 87 | #comments .errorlist { 88 | margin: 0; 89 | padding: 0; 90 | font-size: 12px; 91 | color: #9F3737; 92 | } 93 | #comments .errorlist li { 94 | list-style: none; 95 | margin: 0; 96 | padding: 0; 97 | display: block; 98 | margin-left: 80px; 99 | } 100 | #comments .errorlist li:before { 101 | content: "- "; 102 | } 103 | #comments .errornote { 104 | font-weight: bold; 105 | color: #9F3737; 106 | } 107 | #comments .loader { 108 | margin-left: 80px; 109 | } 110 | #comments .button { 111 | margin-left: 80px; 112 | width: 200px; 113 | height: 28px; 114 | line-height: 20px; 115 | font-size: 11pt; 116 | font-weight: bold; 117 | background-color: #d8d8d8; 118 | color: #111; 119 | border: 1px solid #888; 120 | border-radius: 5px; 121 | cursor: pointer; 122 | } -------------------------------------------------------------------------------- /buzzengine/api/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | import re 9 | import urllib 10 | 11 | from django import newforms as forms 12 | from buzzengine.api import models 13 | from google.appengine.api import taskqueue 14 | 15 | 16 | class NewCommentForm(forms.Form): 17 | article_url = forms.URLField(required=True, widget=forms.HiddenInput) 18 | article_title = forms.CharField(required=False, widget=forms.HiddenInput) 19 | 20 | author_name = forms.CharField(required=True, label="Name") 21 | author_email = forms.EmailField(required=True, label="Email") 22 | author_url = forms.URLField(required=False, label="URL") 23 | author_ip = forms.CharField(required=False) 24 | 25 | comment = forms.CharField(required=True, widget=forms.Textarea(attrs={'cols': 30, 'rows': 3})) 26 | 27 | def save(self): 28 | data = self.clean_data 29 | 30 | article_url = data.get('article_url') 31 | article_title = data.get('article_title') or data.get('article_url') 32 | 33 | article = models.Article.get_or_insert(article_url, url=article_url, title=article_title) 34 | if article.title != article_title: 35 | article.title = article_title 36 | article.put() 37 | 38 | author_email = data.get('author_email') 39 | author_name = data.get('author_name') 40 | author_url = data.get('author_url') 41 | author_ip = data.get('author_ip') 42 | author_key = (author_email or '') + author_name 43 | 44 | author = models.Author.get_or_insert(author_key, name=author_name) 45 | has_changes = False 46 | 47 | if author.url != author_url and author_url: 48 | author.url = author_url 49 | has_changes = True 50 | 51 | if author_email and author_email != author.email: 52 | author.email = author_email 53 | has_changes = True 54 | 55 | if has_changes: 56 | author.put() 57 | 58 | comment = models.Comment(parent=article, comment=data.get('comment'), author=author, article=article, author_ip=author_ip) 59 | comment.put() 60 | 61 | params = urllib.urlencode({'article_url': article_url, 'comment_id': comment.key().id()}) 62 | taskqueue.add(url="/api/notify/?" + params, method="GET") 63 | 64 | self._author = author 65 | self._article = article 66 | self._comment = comment 67 | 68 | return comment 69 | 70 | @property 71 | def output(self): 72 | return { 73 | 'article': { 74 | 'url': self._article.url, 75 | 'title': self._article.title, 76 | }, 77 | 'author': { 78 | 'email': self._author.email, 79 | 'name': self._author.name, 80 | 'url': self._author.url, 81 | }, 82 | 'comment': self._comment.comment, 83 | } 84 | -------------------------------------------------------------------------------- /buzzengine/templates/frontend/admin/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TheBuzzEngine Admin 5 | 6 | 22 | 106 | 107 | 108 | {% block topbar %} 109 |
110 | {% if message %}
{{ message }}
{% endif %} 111 | Logged in as {{ user.email }} [logout] 112 |
113 | {% endblock %} 114 | 115 |

{% block title %}TheBuzzEngine - Admin Panel{% endblock %}

116 | 117 | {% block content %}{% endblock %} 118 | 119 | 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | ======= 3 | 4 | TheBuzzEngine is a simple commenting system, designed for Google App 5 | Engine, as a replacement for DISQUS and similar services. 6 | 7 | NOTE: THIS REPO IS CURRENTLY UNMAINTAINED. New development happens at [alexandru/buzzengine](https://github.com/alexandru/buzzengine/), but I switched the technological stack (the JVM, Scala, regular RDBMS) and it isn't meant to be deployed on Google's App Engine anymore. If anybody is interested in maintaining this repository, let me know. 8 | 9 | Install 10 | ======= 11 | 12 | 1. download the [Google App Engine - Python SDK](http://code.google.com/appengine/downloads.html#Google_App_Engine_SDK_for_Python) 13 | 2. create an instance on Google App Engine - check out their docs on [Getting Started](http://code.google.com/appengine/docs/python/gettingstarted) with Python 14 | 3. fork this repo or download [an archive](https://github.com/alexandru/TheBuzzEngine/zipball/master) 15 | 4. in [appcfg.yml](https://github.com/alexandru/TheBuzzEngine/blob/master/app.yaml) change the "application" (first line) to point to whatever App ID you created 16 | 5. in [settings.py](https://github.com/alexandru/TheBuzzEngine/blob/master/buzzengine/settings.py) change ADMIN_EMAIL + EMAIL_SENDER (see comments) 17 | 6. deploy and go to the homepage, which should look like [this one](http://thebuzzengine.appspot.com/) -- check out this doc on how to [Upload an Application](http://code.google.com/appengine/docs/python/gettingstarted/uploading.html) to GAE 18 | 7. follow the instructions in your app's homepage (you just need to copy a Javascript widget in whatever page you want comments) 19 | 20 | Details 21 | ======= 22 | 23 | I tried to use [Disqus](http://disqus.com) for adding commenting to my 24 | blog pages; but Disqus is too complex and I needed something simpler: 25 | 26 | - 4 fields: Name, Email, URL + Comment 27 | - Those 3 author-related fields should be tracked with a cookie and 28 | auto-completed 29 | - Gravatar for user images 30 | - Moderation (editing or removing) by email 31 | - Fast, light 32 | 33 | TheBuzzEngine is an implementation for the above and it runs on 34 | Google's App Engine. 35 | 36 | Roadmap 37 | ------- 38 | 39 | The following features are planned: 40 | 41 | - email subscriptions to replies in a thread 42 | - threaded replies to comments (only 1 level) 43 | - Askimet integration for spam filtering 44 | - better admin (email moderation is OK for low traffic, but better control and filtering required) 45 | 46 | Cross-domain, Cross-browser requests 47 | ------------------------------------ 48 | 49 | What I did is described in this article: 50 | [http://alexn.org/blog/2011/03/24/cross-domain-requests.html](http://alexn.org/blog/2011/03/24/cross-domain-requests.html) 51 | 52 | Browsers supported 53 | ------------------ 54 | 55 | Tested with Chrome 5, Firefox 3.5, Opera 11, IExplorer 8, IExplorer 6. 56 | 57 | In IExplorer < 8 the commenting form is disabled (meaning only 58 | comments are shown). I may fix this, but incentive for me to fix it 59 | for IExplorer 6 is pretty low. If you want IExplorer 6, send me a note 60 | and I'll reconsider. 61 | 62 | Google App Engine 63 | ----------------- 64 | 65 | It's OK for simple stuff like this -- just one problem for this app -- 66 | unfortunately if there are no warm instances active, request can take 67 | 10 seconds. 68 | 69 | Facilities used thus far: 70 | 71 | - datastore 72 | - memcached 73 | - tasks queue 74 | - sending email 75 | 76 | License 77 | ------- 78 | 79 | MIT Licensed. See the LICENSE file for details. 80 | 81 | -------------------------------------------------------------------------------- /buzzengine/static/js/flXHR/flensed.js: -------------------------------------------------------------------------------- 1 | /* flensedCore 1.0 2 | Copyright (c) 2008 Kyle Simpson, Getify Solutions, Inc. 3 | This software is released under the MIT License 4 | 5 | ==================================================================================================== 6 | Portions of this code were extracted and/or derived from: 7 | 8 | SWFObject v2.1 & 2.2a8 9 | Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis 10 | This software is released under the MIT License 11 | */ 12 | (function(B){var H=B,L=B.document,N="undefined",M=true,C=false,E="",F="object",D="string",G=null,J=null,I=null,K=B.parseInt,A=B.setTimeout;if(typeof B.flensed===N){B.flensed={}}else{if(typeof B.flensed.ua!==N){return}}G=B.flensed;A(function(){var P="flensed.js",U=C,R=L.getElementsByTagName("script"),T=R.length;try{G.base_path.toLowerCase();U=M}catch(S){G.base_path=E}if((typeof R!==N)&&(R!==null)){if(!U){var O=0;for(var Q=0;Q=0){G.base_path=R[Q].src.substr(0,O);break}}}}}},0);G.parseXMLString=function(P){var O=null;if(H.ActiveXObject){O=new B.ActiveXObject("Microsoft.XMLDOM");O.async=C;O.loadXML(P)}else{var Q=new B.DOMParser();O=Q.parseFromString(P,"text/xml")}return O};G.getObjectById=function(O){try{if(L.layers){return L.layers[O]}else{if(L.all){return L.all[O]}else{if(L.getElementById){return L.getElementById(O)}}}}catch(P){}return null};G.createCSS=function(T,P,U,S){if(G.ua.ie&&G.ua.mac){return}var R=L.getElementsByTagName("head")[0];if(!R){return}var O=(U&&typeof U===D)?U:"screen";if(S){J=null;I=null}if(!J||I!==O){var Q=L.createElement("style");Q.setAttribute("type","text/css");Q.setAttribute("media",O);J=R.appendChild(Q);if(G.ua.ie&&G.ua.win&&typeof L.styleSheets!==N&&L.styleSheets.length>0){J=L.styleSheets[L.styleSheets.length-1]}I=O}if(G.ua.ie&&G.ua.win){if(J&&typeof J.addRule===F){J.addRule(T,P)}}else{if(J&&typeof L.createTextNode!==N){J.appendChild(L.createTextNode(T+" {"+P+"}"))}}};G.bindEvent=function(R,O,Q){O=O.toLowerCase();try{if(typeof R.addEventListener!==N){R.addEventListener(O.replace(/^on/,E),Q,C)}else{if(typeof R.attachEvent!==N){R.attachEvent(O,Q)}}}catch(P){}};G.unbindEvent=function(R,O,Q){O=O.toLowerCase();try{if(typeof R.removeEventListener!==N){R.removeEventListener(O.replace(/^on/,E),Q,C)}else{if(typeof R.detachEvent!==N){R.detachEvent(O,Q)}}}catch(P){}};G.throwUnhandledError=function(O){throw new B.Error(O)};G.error=function(R,P,Q,O){return{number:R,name:P,description:Q,message:Q,srcElement:O,toString:function(){return R+", "+P+", "+Q}}};G.ua=function(){var U="Shockwave Flash",O="ShockwaveFlash.ShockwaveFlash",Y="application/x-shockwave-flash",P=B.navigator,V=typeof L.getElementById!==N&&typeof L.getElementsByTagName!==N&&typeof L.createElement!==N,f=[0,0,0],X=null;if(typeof P.plugins!==N&&typeof P.plugins[U]===F){X=P.plugins[U].description;if(X&&!(typeof P.mimeTypes!==N&&P.mimeTypes[Y]&&!P.mimeTypes[Y].enabledPlugin)){X=X.replace(/^.*\s+(\S+\s+\S+$)/,"$1");f[0]=K(X.replace(/^(.*)\..*$/,"$1"),10);f[1]=K(X.replace(/^.*\.(.*)\s.*$/,"$1"),10);f[2]=/r/.test(X)?K(X.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof H.ActiveXObject!==N){try{var Z=new B.ActiveXObject(O);if(Z){X=Z.GetVariable("$version");if(X){X=X.split(" ")[1].split(",");f=[K(X[0],10),K(X[1],10),K(X[2],10)]}}}catch(T){}}}var e=P.userAgent.toLowerCase(),S=P.platform.toLowerCase(),c=/webkit/.test(e)?parseFloat(e.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):C,Q=C,R=0,b=S?/win/.test(S):/win/.test(e),W=S?/mac/.test(S):/mac/.test(e);/*@cc_on Q=M;try{R=K(e.match(/msie (\d+)/)[1],10);}catch(e2){}@if(@_win32)b=M;@elif(@_mac)W=M;@end @*/return{w3cdom:V,pv:f,webkit:c,ie:Q,ieVer:R,win:b,mac:W}}()})(window); 13 | -------------------------------------------------------------------------------- /buzzengine/api/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | import random 9 | import hashlib 10 | 11 | from django.conf import settings 12 | from google.appengine.api import memcache 13 | from google.appengine.ext import webapp 14 | from google.appengine.ext import db 15 | 16 | 17 | CACHE_EXP_SECS = 60 * 60 * 24 * 7 * 4 # 4 weeks 18 | 19 | 20 | class Article(db.Model): 21 | url = db.LinkProperty(required=True) 22 | title = db.StringProperty(required=False) 23 | 24 | created_at = db.DateTimeProperty(auto_now_add=True) 25 | 26 | def __unicode__(self): 27 | return str(self.url) 28 | 29 | 30 | class Author(db.Model): 31 | name = db.StringProperty(required=True) 32 | email = db.EmailProperty(required=False) 33 | url = db.StringProperty(required=False) 34 | email_hash = db.StringProperty(required=False) 35 | created_at = db.DateTimeProperty(auto_now_add=True) 36 | 37 | def __unicode__(self): 38 | return "%s (%s)" % (self.name, self.email or "no email specified") 39 | 40 | def put(self): 41 | email = self.email and self.email.strip() 42 | name = self.name.strip() 43 | 44 | md5 = hashlib.md5() 45 | md5.update(email or name) 46 | 47 | email_hash = md5.hexdigest() 48 | self.email_hash = email_hash 49 | 50 | obj = super(Author, self).put() 51 | memcache.delete(self.email_hash, namespace="authors") 52 | return obj 53 | 54 | def get_email_hash(self): 55 | if not self.email_hash: 56 | self.put() 57 | return self.email_hash 58 | 59 | @property 60 | def gravatar_url(self): 61 | return "http://www.gravatar.com/avatar/" + self.get_email_hash() + ".jpg?s=80&d=mm" 62 | 63 | @classmethod 64 | def get_by_hash(self, email_hash): 65 | author = memcache.get(email_hash, namespace="authors") 66 | if not author: 67 | author = Author.gql("WHERE email_hash = :1", email_hash)[:1] 68 | author = author[0] if author else None 69 | memcache.set(email_hash, author, time=CACHE_EXP_SECS, namespace='authors') 70 | return author 71 | 72 | 73 | class Comment(db.Model): 74 | article = db.ReferenceProperty(Article, required=True) 75 | author = db.ReferenceProperty(Author, required=True) 76 | comment = db.TextProperty(required=True) 77 | author_ip = db.StringProperty(required=False) 78 | 79 | created_at = db.DateTimeProperty(auto_now_add=True) 80 | updated_at = db.DateTimeProperty(auto_now=True) 81 | 82 | def delete(self, *args, **kwargs): 83 | # invalidates cache 84 | memcache.delete(self.article.url, namespace='comments') 85 | return super(Comment, self).delete(*args, **kwargs) 86 | 87 | def put(self, *args, **kwargs): 88 | obj = super(Comment, self).put(*args, **kwargs) 89 | # invalidates cache 90 | memcache.delete(self.article.url, namespace='comments') 91 | return obj 92 | 93 | @classmethod 94 | def get_comments(self, article_url): 95 | comments = memcache.get(article_url, namespace="comments") 96 | 97 | if not comments: 98 | article = Article.get_by_key_name(article_url) 99 | if article: 100 | comments = Comment.gql("WHERE article = :1", article) 101 | comments = [ {'id': c.key().id(), 'comment': c.comment, 'created_at': c.created_at, "author": { "name": c.author.name, 'url': c.author.url, 'email': c.author.email, 'gravatar_url': c.author.gravatar_url }} for c in comments ] 102 | else: 103 | comments = [] 104 | 105 | memcache.set(article_url, comments, time=CACHE_EXP_SECS, namespace='comments') 106 | 107 | return comments 108 | -------------------------------------------------------------------------------- /buzzengine/static/css/syntax.css: -------------------------------------------------------------------------------- 1 | .syntax .hll { background-color: #404040 } 2 | .syntax { background: #202020; color: #d0d0d0 } 3 | .syntax .c { color: #999999; font-style: italic } /* Comment */ 4 | .syntax .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 5 | .syntax .g { color: #d0d0d0 } /* Generic */ 6 | .syntax .k { color: #6ab825; font-weight: bold } /* Keyword */ 7 | .syntax .l { color: #d0d0d0 } /* Literal */ 8 | .syntax .n { color: #d0d0d0 } /* Name */ 9 | .syntax .o { color: #d0d0d0 } /* Operator */ 10 | .syntax .x { color: #d0d0d0 } /* Other */ 11 | .syntax .p { color: #d0d0d0 } /* Punctuation */ 12 | .syntax .cm { color: #999999; font-style: italic } /* Comment.Multiline */ 13 | .syntax .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */ 14 | .syntax .c1 { color: #999999; font-style: italic } /* Comment.Single */ 15 | .syntax .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ 16 | .syntax .gd { color: #d22323 } /* Generic.Deleted */ 17 | .syntax .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ 18 | .syntax .gr { color: #d22323 } /* Generic.Error */ 19 | .syntax .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ 20 | .syntax .gi { color: #589819 } /* Generic.Inserted */ 21 | .syntax .go { color: #cccccc } /* Generic.Output */ 22 | .syntax .gp { color: #aaaaaa } /* Generic.Prompt */ 23 | .syntax .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */ 24 | .syntax .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ 25 | .syntax .gt { color: #d22323 } /* Generic.Traceback */ 26 | .syntax .kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */ 27 | .syntax .kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */ 28 | .syntax .kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */ 29 | .syntax .kp { color: #6ab825 } /* Keyword.Pseudo */ 30 | .syntax .kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */ 31 | .syntax .kt { color: #6ab825; font-weight: bold } /* Keyword.Type */ 32 | .syntax .ld { color: #d0d0d0 } /* Literal.Date */ 33 | .syntax .m { color: #3677a9 } /* Literal.Number */ 34 | .syntax .s { color: #ed9d13 } /* Literal.String */ 35 | .syntax .na { color: #bbbbbb } /* Name.Attribute */ 36 | .syntax .nb { color: #24909d } /* Name.Builtin */ 37 | .syntax .nc { color: #447fcf; text-decoration: underline } /* Name.Class */ 38 | .syntax .no { color: #40ffff } /* Name.Constant */ 39 | .syntax .nd { color: #ffa500 } /* Name.Decorator */ 40 | .syntax .ni { color: #d0d0d0 } /* Name.Entity */ 41 | .syntax .ne { color: #bbbbbb } /* Name.Exception */ 42 | .syntax .nf { color: #447fcf } /* Name.Function */ 43 | .syntax .nl { color: #d0d0d0 } /* Name.Label */ 44 | .syntax .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */ 45 | .syntax .nx { color: #d0d0d0 } /* Name.Other */ 46 | .syntax .py { color: #d0d0d0 } /* Name.Property */ 47 | .syntax .nt { color: #6ab825; font-weight: bold } /* Name.Tag */ 48 | .syntax .nv { color: #40ffff } /* Name.Variable */ 49 | .syntax .ow { color: #6ab825; font-weight: bold } /* Operator.Word */ 50 | .syntax .w { color: #666666 } /* Text.Whitespace */ 51 | .syntax .mf { color: #3677a9 } /* Literal.Number.Float */ 52 | .syntax .mh { color: #3677a9 } /* Literal.Number.Hex */ 53 | .syntax .mi { color: #3677a9 } /* Literal.Number.Integer */ 54 | .syntax .mo { color: #3677a9 } /* Literal.Number.Oct */ 55 | .syntax .sb { color: #ed9d13 } /* Literal.String.Backtick */ 56 | .syntax .sc { color: #ed9d13 } /* Literal.String.Char */ 57 | .syntax .sd { color: #ed9d13 } /* Literal.String.Doc */ 58 | .syntax .s2 { color: #ed9d13 } /* Literal.String.Double */ 59 | .syntax .se { color: #ed9d13 } /* Literal.String.Escape */ 60 | .syntax .sh { color: #ed9d13 } /* Literal.String.Heredoc */ 61 | .syntax .si { color: #ed9d13 } /* Literal.String.Interpol */ 62 | .syntax .sx { color: #ffa500 } /* Literal.String.Other */ 63 | .syntax .sr { color: #ed9d13 } /* Literal.String.Regex */ 64 | .syntax .s1 { color: #ed9d13 } /* Literal.String.Single */ 65 | .syntax .ss { color: #ed9d13 } /* Literal.String.Symbol */ 66 | .syntax .bp { color: #24909d } /* Name.Builtin.Pseudo */ 67 | .syntax .vc { color: #40ffff } /* Name.Variable.Class */ 68 | .syntax .vg { color: #40ffff } /* Name.Variable.Global */ 69 | .syntax .vi { color: #40ffff } /* Name.Variable.Instance */ 70 | .syntax .il { color: #3677a9 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /buzzengine/static/js/buzzengine.min.js: -------------------------------------------------------------------------------- 1 | (function(){var f={};var w={};var d={};function g(){var D=document.getElementById("comments");return D.getAttribute("data-domain")}function e(D){return Object.prototype.toString.call(D).match(/^\[object (.*)\]$/)[1]}function n(){return navigator.userAgent.indexOf("MSIE")!=-1}function b(){var F=-1;if(navigator.appName=="Microsoft Internet Explorer"){var D=navigator.userAgent;var E=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");if(E.exec(D)!=null){F=parseFloat(RegExp.$1)}}return F}function r(D){return(e(D)!="String"?D:document.getElementById(D))}function v(D){r(D).style.display="block"}function m(D){r(D).style.display="none"}function q(E){E=r(E);if(E==document){return true}if(!E){return false}if(!E.parentNode){return false}if(E.style){if(E.style.display=="none"){return false}if(E.style.visibility=="hidden"){return false}}if(window.getComputedStyle){var D=window.getComputedStyle(E,"");if(D.display=="none"){return false}if(D.visibility=="hidden"){return false}}var D=E.currentStyle;if(D){if(D.display=="none"){return false}if(D.visibility=="hidden"){return false}}return isVisible(E.parentNode)}function C(G,D,E){var F=r(G);if(F.addEventListener&&!n()){F.addEventListener(D,E,false)}else{if(F.addEventListener){F.addEventListener(D,E)}else{if(F.attachEvent){F.attachEvent("on"+D,E)}}}}function z(E,D){r(E).setAttribute("class",D)}function x(N){var G=N.name;var M=N.type;var D=N.tag;var K=r("comments");var E=D?[D]:["input","textarea","button"];for(var I=0;I0){var E=d[D].pop();if(E){E()}}}function l(E,F){s(E,F);if(f[E]){A(E);return true}if(w[E]){return false}w[E]=true;var D=document.getElementsByTagName("head")[0];js=document.createElement("script");js.setAttribute("type","text/javascript");js.setAttribute("src",E);D.appendChild(js);js.onreadystatechange=function(){if(js.readyState=="complete"||js.readyState=="loaded"){if(!f[E]){f[E]=true;A(E)}}};js.onload=function(){if(!f[E]){f[E]=true;A(E)}};return false}function k(F){var E=F.url;var G=F.type||"GET";var J=F.success;var D=F.error;var I=F.data;function H(L){if(L.readyState==4){if(L.status==200&&J){J(L.responseText,L)}else{D(L)}}}var K=new flensed.flXHR({autoUpdatePlayer:false,instanceId:"myproxy1",xmlResponseText:false,onreadystatechange:H,loadPolicyURL:"http://"+g()+"/static/load-policy.xml"});K.open(G,E);K.send(I)}function o(M){var D=M.url;var I=M.type||"GET";var K=M.success;var J=M.error;var G=M.data;try{var L=new XMLHttpRequest()}catch(H){}var E=false;if(L&&"withCredentials" in L){L.open(I,D,true)}else{if(typeof XDomainRequest!="undefined"){L=new XDomainRequest();L.open(I,D)}else{L=null}}if(!L){if(typeof flensed=="undefined"){l("http://"+g()+"/static/js/flXHR/flXHR.js",function(){k(M)})}else{k(M)}}else{var F=function(N){return function(O){var O=n()?L:O;if(N=="load"&&(n()||O.readyState==4)&&K){K(O.responseText,O)}else{if(J){J(O)}}}};try{L.withCredentials=true}catch(H){}L.onload=function(N){F("load")(n()?N:N.target)};L.onerror=function(N){F("error")(n()?N:N.target)};L.send(G)}}function y(D){if(D){document.getElementById("comments").innerHTML=D}if(n()&&b()<8){m(document.getElementById("comments").getElementsByTagName("form")[0]);return}setTimeout(function(){x({name:"article_url"}).setAttribute("value",i());x({name:"article_title"}).setAttribute("value",j());var E=h();if(E){p("author",E,365)}var F=r("comments").getElementsByTagName("form")[0];C(F,"submit",function(G){if(n()){G.returnValue=false}else{if(G.preventDefault){G.preventDefault()}}v(t("comments","loader"));m(t("comments","button"));o({url:F.getAttribute("action"),type:"POST",data:c(),success:function(H){y(H)},error:function(H){t("comments","errornote").remove();F.innerHTML="
Something wrong happened, please try again later!
"+F.innerHTML}});return false})},0)}function u(){var E=a("author");E=E?encodeURI(E):null;var D="http://"+g()+"/api/comments/";D+="?article_url="+encodeURI(window.location+"");if(E){D+="&author="+encodeURI(E)}o({url:D,type:"GET",success:function(F){y(F)}})}u()})(); 2 | 3 | -------------------------------------------------------------------------------- /buzzengine/api/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Alexandru Nedelcu" 5 | __email__ = "noreply@alexn.org" 6 | 7 | 8 | import hashlib 9 | import re 10 | 11 | from datetime import datetime, timedelta 12 | from django.utils import simplejson as json 13 | 14 | from google.appengine.api import mail 15 | from django.template.loader import get_template 16 | from django.template import Context 17 | from django.http import HttpResponse, HttpResponseRedirect, Http404 18 | from django.shortcuts import render_to_response 19 | from django.conf import settings 20 | from buzzengine.api.forms import NewCommentForm 21 | from buzzengine.api import models 22 | 23 | 24 | def comments(request): 25 | url = request.REQUEST.get('article_url') or request.META.get('HTTP_REFERER') 26 | if not url: 27 | raise Http404 28 | 29 | if url.rfind('#') > 0: 30 | url = url[:url.rfind('#')] 31 | 32 | if not url: 33 | resp = HttpResponse(json.dumps({'article_url': ['This field is required.']}, indent=4), mimetype='text/plain') 34 | resp.status_code = 400 35 | return resp 36 | 37 | if request.method == 'POST': 38 | return _comment_create(request, url) 39 | else: 40 | return _comment_list(request, url) 41 | 42 | 43 | def _comment_create(request, article_url): 44 | data = request.POST 45 | data = dict([ (k,data[k]) for k in data.keys() ]) 46 | 47 | data['article_url'] = article_url 48 | data['author_ip'] = _discover_user_ip(request) 49 | 50 | is_json = not request.META['HTTP_ACCEPT'].find("html") 51 | 52 | form = NewCommentForm(data) 53 | if not form.is_valid(): 54 | if is_json: 55 | resp = HttpResponse(json.dumps(form.errors, indent=4), mimetype='text/plain') 56 | resp.status_code = 400 57 | return resp 58 | else: 59 | return _comment_list(request, article_url, form=form) 60 | 61 | new_comment = form.save() 62 | if is_json: 63 | response = HttpResponse("OK", mimetype='text/plain') 64 | 65 | return _comment_list(request, article_url=article_url, author=new_comment.author) 66 | 67 | 68 | def _comment_list(request, article_url, form=None, author=None): 69 | 70 | comments = models.Comment.get_comments(article_url) 71 | 72 | is_json = not request.META['HTTP_ACCEPT'].find("html") 73 | if is_json: 74 | comments = [ {'comment': c['comment'], "author": { "name": c['author']['name'], 'url': c['author']['url'], 'gravatar_url': c['author']['gravatar_url'] }} for c in comments ] 75 | return HttpResponse(json.dumps(comments, indent=4), mimetype="text/plain") 76 | 77 | data = request.POST 78 | data = dict([ (k,data[k]) for k in data.keys() ]) 79 | data['article_url'] = article_url 80 | data['comment'] = None 81 | 82 | author = author or request.author 83 | if author and not (data.get('author_name') or data.get('author_email') or data.get('author_url')): 84 | data['author_name'] = author.name 85 | data['author_email'] = author.email 86 | data['author_url'] = author.url 87 | 88 | comments.sort(lambda a,b: cmp(a['created_at'], b['created_at'])) 89 | 90 | form = form or NewCommentForm(initial=data) 91 | return render_to_response("api/comments.html", {'comments': comments, 'form': form, 'API_DOMAIN': request.API_DOMAIN, 'current_author': author}) 92 | 93 | 94 | def _discover_user_ip(request): 95 | # discover IP of user 96 | ip = request.META['REMOTE_ADDR'] 97 | if ip == '127.0.0.1': 98 | try: 99 | ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0] 100 | except: 101 | pass 102 | return ip 103 | 104 | def say_hello(request): 105 | return HttpResponse("TheBuzzEngine greeted with %s, says hello!" % request.method, mimetype="text/html") 106 | 107 | def test_page(request): 108 | return render_to_response("api/test_page.html", { 'API_DOMAIN': request.API_DOMAIN }) 109 | 110 | 111 | def crossdomain_xml(request): 112 | return HttpResponseRedirect("/static/local-policy.xml") 113 | 114 | def export_xml(request): 115 | articles = models.Article.all() 116 | output = """ 117 | 118 | 119 | """ 120 | for article in articles: 121 | output += " \n" 122 | output += """ 123 | %s 124 | %s 125 | %s 126 | %s 127 | open 128 | """ % (article.title or article.url, article.url or '', article.url or '', article.created_at.strftime("%Y-%m-%d %H:%M:%S")) 129 | 130 | comments = models.Comment.get_comments(article.url) 131 | for comment in comments: 132 | output += " \n" 133 | output += """ 134 | %s 135 | %s 136 | %s 137 | %s 138 | %s 139 | 140 | 1 141 | 0""" % ( 142 | 143 | comment['id'], 144 | comment['author'].get('name') or '', 145 | comment['author'].get('email') or '', 146 | comment['author'].get('url') or '', 147 | comment['created_at'].strftime('%Y-%m-%d %H:%M:%S'), 148 | comment['comment'], 149 | 150 | ) 151 | output += " \n" 152 | 153 | output += " \n" 154 | 155 | output += """ 156 | """ 157 | 158 | return HttpResponse(output, mimetype="text/plain") 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /buzzengine/templates/frontend/homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TheBuzzEngine - About 5 | 6 | 7 | 81 | 82 | 83 |

TheBuzzEngine

84 | Fork me on GitHub 85 | 86 |

It works!

87 | 88 |

89 | Copy/paste the following script to your website, inside the HTML's <body>, where you want comments to be shown: 90 |

91 | 92 |
    
 93 |     <div id="comments" data-domain="{{ API_DOMAIN }}"></div> 
 94 |     <script language="javascript" type="text/javascript"> 
 95 |       var BUZZENGINE_PARAMS = { 
 96 | 	  article_url:   window.location + '', 
 97 | 	  article_title: document.getElementsByTagName('title').length ? 
 98 | 	      document.getElementsByTagName('title')[0].innerHTML : null 
 99 |       }; 
100 |  
101 |       (function() { 
102 |           var s = document.createElement('script'); 
103 |           s.type = 'text/javascript'; 
104 |           s.async = true; 
105 |           s.src = 'http://{{ API_DOMAIN }}/static/js/buzzengine.min.js'; 
106 |           var x = document.getElementsByTagName('script')[0]; 
107 |           x.parentNode.insertBefore(s, x); 
108 |       })();      
109 |     </script>
110 | 
111 | 
112 | 113 | And if you want a good-enough CSS stylesheet that you could further customize, copy/paste this piece to your HTML's <head>: 114 |
    
115 |     <link href="http://{{ API_DOMAIN }}/static/css/comments.css" 
116 |       rel="stylesheet" type="text/css" />
117 | 
118 | 
119 | 120 |

Testing Comments

121 | 122 |
123 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /buzzengine/static/js/flXHR/checkplayer.js: -------------------------------------------------------------------------------- 1 | /* CheckPlayer 1.0.2 2 | Copyright (c) 2008 Kyle Simpson, Getify Solutions, Inc. 3 | This software is released under the MIT License 4 | 5 | ==================================================================================================== 6 | Portions of this code were extracted and/or derived from: 7 | 8 | SWFObject v2.1 & 2.2a8 9 | Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis 10 | This software is released under the MIT License 11 | */ 12 | (function(R){var E=R,V=R.document,N="undefined",G=true,X=false,W="",H="object",O="function",T="string",M="div",D="onunload",J="none",U=null,P=null,I=null,L=null,K="flensed.js",F="checkplayer.js",B="swfobject.js",C=R.setTimeout,A=R.clearTimeout,S=R.setInterval,Q=R.clearInterval;if(typeof R.flensed===N){R.flensed={}}if(typeof R.flensed.checkplayer!==N){return }P=R.flensed;C(function(){var Y=X,i=V.getElementsByTagName("script"),d=i.length;try{P.base_path.toLowerCase();Y=G}catch(b){P.base_path=""}function g(o,n,p){for(var m=0;m=0){break}}}var l=V.createElement("script");l.setAttribute("src",P.base_path+o);if(typeof n!==N){l.setAttribute("type",n)}if(typeof p!==N){l.setAttribute("language",p)}V.getElementsByTagName("head")[0].appendChild(l)}if((typeof i!==N)&&(i!==null)){if(!Y){var j=0;for(var c=0;c=0)||((j=i[c].src.indexOf(F))>=0)){P.base_path=i[c].src.substr(0,j);break}}}}}try{R.swfobject.getObjectById("a")}catch(h){g(B,"text/javascript")}try{P.ua.pv.join(".")}catch(f){g(K,"text/javascript")}function Z(){A(a);try{E.detachEvent(D,arguments.callee)}catch(k){}}try{E.attachEvent(D,Z)}catch(e){}var a=C(function(){Z();try{R.swfobject.getObjectById("a");P.ua.pv.join(".")}catch(k){throw new R.Error("CheckPlayer dependencies failed to load.")}},20000)},0);P.checkplayer=function(x,AI,o,AB){if(typeof I._ins!==N){if(I._ins.ready()){setTimeout(function(){AI(I._ins)},0)}return I._ins}var a="6.0.65",z=[],i=null,f=X,g=null,AK=null,s=W,d=X,l=null,b=[],r={},AA=[],e=null,AG=null,AF=null,m=null,h=X,AH=null,k=X,t=X,p=X,AE=null;var Z=function(){if((typeof x!==N)&&(x!==null)&&(x!==X)){AG=x+W}else{AG="0.0.0"}if(typeof AI===O){AF=AI}if(typeof o!==N){h=!(!o)}if(typeof AB===O){m=AB}function AM(){A(g);try{E.detachEvent(D,AM)}catch(AP){}}try{E.attachEvent(D,AM)}catch(AN){}(function AO(){try{P.bindEvent(E,D,y)}catch(AP){g=C(arguments.callee,25);return }AM();AH=P.ua.pv.join(".");g=C(AD,1)})()}();function AD(){try{e=V.getElementsByTagName("body")[0]}catch(AN){}if((typeof e===N)||(e===null)){g=C(AD,25);return }try{R.swfobject.getObjectById("a");L=R.swfobject}catch(AM){g=C(AD,25);return }t=L.hasFlashPlayerVersion(a);k=L.hasFlashPlayerVersion(AG);AJ();if(typeof AF===O){AF(j)}d=G;if(k){u()}else{if(h&&!f){v()}}}function y(){if(typeof E.detachEvent!==N){E.detachEvent(D,y)}I._ins=null;if((typeof l!==N)&&(l!==null)){try{l.updateSWFCallback=null;AC=null}catch(AP){}l=null}try{for(var AO in j){if(j[AO]!==Object.prototype[AO]){try{j[AO]=null}catch(AN){}}}}catch(AM){}j=null;e=null;Y();AA=null;AF=null;m=null;try{for(var AS in I){if(I[AS]!==Object.prototype[AS]){try{I[AS]=null}catch(AR){}}}}catch(AQ){}I=null;P.checkplayer=null;P=null;R=null}function AL(AN,AO,AM){AA[AA.length]={func:AN,funcName:AO,args:AM}}function u(){if(!d){i=C(u,25);return }var AO=0;try{AO=AA.length}catch(AP){}for(var AN=0;AN0)){Aa=Ad.swfTimeout}if(typeof Ad.swfEICheck!==N&&Ad.swfEICheck!==null&&Ad.swfEICheck!==W){AM=Ad.swfEICheck}}else{if(typeof Ad===O){AX=Ad}}}try{AV=L.createSWF(AZ,AY,Ag)}catch(AQ){}if(AV!==null){b[b.length]=Ae;if(typeof AX===O){AX({status:I.SWF_INIT,srcId:Ae,srcElem:AV});r[Ae]=S(function(){var Ai=P.getObjectById(Ae);if((typeof Ai!==N)&&(Ai!==null)&&(Ai.nodeName==="OBJECT"||Ai.nodeName==="EMBED")){var Ah=0;try{Ah=Ai.PercentLoaded()}catch(Aj){}if(Ah>0){if(Aa>0){A(r["DoSWFtimeout_"+Ae]);r["DoSWFtimeout_"+Ae]=X}if(Ah<100){C(function(){AX({status:I.SWF_LOADING,srcId:Ae,srcElem:Ai})},1)}else{Q(r[Ae]);r[Ae]=X;C(function(){AX({status:I.SWF_LOADED,srcId:Ae,srcElem:Ai})},1);if(AM!==W){var Ak=X;r[Ae]=S(function(){if(!Ak&&typeof Ai[AM]===O){Ak=G;try{Ai[AM]();Q(r[Ae]);r[Ae]=X;AX({status:I.SWF_EI_READY,srcId:Ae,srcElem:Ai})}catch(Al){}Ak=X}},25)}}}}},50);if(Aa>0){r["DoSWFtimeout_"+Ae]=C(function(){var Ai=P.getObjectById(Ae);if((typeof Ai!==N)&&(Ai!==null)&&(Ai.nodeName==="OBJECT"||Ai.nodeName==="EMBED")){var Ah=0;try{Ah=Ai.PercentLoaded()}catch(Aj){}if(Ah<=0){Q(r[Ae]);r[Ae]=X;if(P.ua.ie&&P.ua.win&&Ai.readyState!==4){Ai.id="removeSWF_"+Ai.id;Ai.style.display=J;r[Ai.id]=S(function(){if(Ai.readyState===4){Q(r[Ai.id]);r[Ai.id]=X;L.removeSWF(Ai.id)}},500)}else{L.removeSWF(Ai.id)}r[Ae]=X;r["DoSWFtimeout_"+Ae]=X;AX({status:I.SWF_TIMEOUT,srcId:Ae,srcElem:Ai})}}},Aa)}}}else{if(typeof AX===O){AX({status:I.SWF_FAILED,srcId:Ae,srcElem:null})}else{throw new R.Error("checkplayer::DoSWF(): SWF could not be loaded.")}}}else{if(typeof AX===O){AX({status:I.SWF_FAILED,srcId:Ae,srcElem:null})}else{throw new R.Error("checkplayer::DoSWF(): Minimum Flash Version not detected.")}}}var j={playerVersionDetected:AH,versionChecked:AG,checkPassed:k,UpdatePlayer:v,DoSWF:function(AR,AS,AP,AQ,AN,AM,AO,AT){q(AR,AS,AP,AQ,AN,AM,AO,AT,X)},ready:function(){return d},updateable:t,updateStatus:p,updateControlsContainer:AE};I._ins=j;return j};I=P.checkplayer;I.UPDATE_INIT=1;I.UPDATE_SUCCESSFUL=2;I.UPDATE_CANCELED=3;I.UPDATE_FAILED=4;I.SWF_INIT=5;I.SWF_LOADING=6;I.SWF_LOADED=7;I.SWF_FAILED=8;I.SWF_TIMEOUT=9;I.SWF_EI_READY=10;I.module_ready=function(){}})(window); -------------------------------------------------------------------------------- /buzzengine/static/js/flXHR/swfobject.js: -------------------------------------------------------------------------------- 1 | /* SWFObject v2.1 2 | Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis 3 | This software is released under the MIT License 4 | */ 5 | var swfobject=function(){var b="undefined",Q="object",n="Shockwave Flash",p="ShockwaveFlash.ShockwaveFlash",P="application/x-shockwave-flash",m="SWFObjectExprInst",j=window,K=document,T=navigator,o=[],N=[],i=[],d=[],J,Z=null,M=null,l=null,e=false,A=false;var h=function(){var v=typeof K.getElementById!=b&&typeof K.getElementsByTagName!=b&&typeof K.createElement!=b,AC=[0,0,0],x=null;if(typeof T.plugins!=b&&typeof T.plugins[n]==Q){x=T.plugins[n].description;if(x&&!(typeof T.mimeTypes!=b&&T.mimeTypes[P]&&!T.mimeTypes[P].enabledPlugin)){x=x.replace(/^.*\s+(\S+\s+\S+$)/,"$1");AC[0]=parseInt(x.replace(/^(.*)\..*$/,"$1"),10);AC[1]=parseInt(x.replace(/^.*\.(.*)\s.*$/,"$1"),10);AC[2]=/r/.test(x)?parseInt(x.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof j.ActiveXObject!=b){var y=null,AB=false;try{y=new ActiveXObject(p+".7")}catch(t){try{y=new ActiveXObject(p+".6");AC=[6,0,21];y.AllowScriptAccess="always"}catch(t){if(AC[0]==6){AB=true}}if(!AB){try{y=new ActiveXObject(p)}catch(t){}}}if(!AB&&y){try{x=y.GetVariable("$version");if(x){x=x.split(" ")[1].split(",");AC=[parseInt(x[0],10),parseInt(x[1],10),parseInt(x[2],10)]}}catch(t){}}}}var AD=T.userAgent.toLowerCase(),r=T.platform.toLowerCase(),AA=/webkit/.test(AD)?parseFloat(AD.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,q=false,z=r?/win/.test(r):/win/.test(AD),w=r?/mac/.test(r):/mac/.test(AD);/*@cc_on q=true;@if(@_win32)z=true;@elif(@_mac)w=true;@end@*/return{w3cdom:v,pv:AC,webkit:AA,ie:q,win:z,mac:w}}();var L=function(){if(!h.w3cdom){return }f(H);if(h.ie&&h.win){try{K.write("