├── 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 |
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 |
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 |
2 | {{ current_author.email_hash }}
3 |
4 |
5 | {% for obj in comments %}
6 |
24 | {% endfor %}
25 |
26 |
--------------------------------------------------------------------------------
/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("