@@ -1,5 +1,7 @@ | |||
{% extends 'base.html' %} {% block content %} | |||
{% extends 'base.html' %}{% block content %} | |||
{% load hitcount_tags %} | |||
<div class="post"> | |||
{% if post.published_date %} | |||
<div class="date"> | |||
{{ post.published_date }} | |||
@@ -17,6 +19,7 @@ | |||
<h1>{{ post.title }}</h1> | |||
<p>{{ post.text|linebreaksbr }}</p> | |||
<p> | |||
Tags: | |||
{% for tag in post.tags.all %} | |||
<a href="{% url 'blog_search_list_view' %}">{{ tag.name }}, </a> | |||
{% endfor %} | |||
@@ -24,5 +27,10 @@ | |||
<p> | |||
Autor: {{ post.author }} | |||
</p> | |||
{% if user.is_staff %} | |||
<p> | |||
{% get_hit_count for post %} Benutzer haben diesen Post bereits gelesen! | |||
</p> | |||
{% endif %} | |||
</div> | |||
{% endblock %} |
@@ -16,6 +16,8 @@ from django.contrib import messages | |||
from post_office.models import EmailTemplate | |||
from post_office import mail | |||
from hitcount.models import HitCount | |||
from hitcount.views import HitCountMixin | |||
import logging | |||
@@ -61,7 +63,9 @@ def post_list(request, slug=None): | |||
@login_required | |||
def post_detail(request, pk): | |||
post = get_object_or_404(Post, pk=pk) | |||
return render(request, 'post_detail.html', {'post': post}) | |||
hit_count = HitCount.objects.get_for_object(post) | |||
hit_count_response = HitCountMixin.hit_count(request, hit_count) | |||
return render(request, 'post_detail.html', locals()) | |||
@login_required | |||
@@ -182,17 +186,10 @@ def blog_search_list_view(request): | |||
def tag_cloud(request): | |||
return render(request, 'tag_cloud.html', {}) | |||
EmailTemplate.objects.create( | |||
name='weekly-update', | |||
subject='Hi' + User.objects.get(username=request.user) + '!', | |||
content='How are you feeling today?' + Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date'), | |||
html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?', | |||
) | |||
mail.send( | |||
'kleinhenz.e@gmail.com', # List of email addresses also accepted | |||
'esther.kleinhenz@web.de', | |||
template='weekly-update', | |||
context={'name': 'alice'}, | |||
subject='My email', | |||
message='Hi there!', | |||
html_message='Hi <strong>there</strong>!', | |||
) |
@@ -48,6 +48,7 @@ INSTALLED_APPS = [ | |||
'taggit_templatetags2', | |||
'kombu.transport.django', | |||
'post_office', | |||
'hitcount', | |||
] | |||
MIDDLEWARE = [ |
@@ -0,0 +1,38 @@ | |||
django-hitcount | |||
=============== | |||
.. image:: https://travis-ci.org/thornomad/django-hitcount.svg?branch=master | |||
:target: https://travis-ci.org/thornomad/django-hitcount | |||
.. image:: https://coveralls.io/repos/thornomad/django-hitcount/badge.svg?branch=master | |||
:target: https://coveralls.io/r/thornomad/django-hitcount?branch=master | |||
.. image:: https://badge.fury.io/py/django-hitcount.svg | |||
:target: http://badge.fury.io/py/django-hitcount | |||
.. image:: https://requires.io/github/thornomad/django-hitcount/requirements.svg?branch=develop | |||
:target: https://requires.io/github/thornomad/django-hitcount/requirements/?branch=develop | |||
:alt: Requirements Status | |||
Basic app that allows you to track the number of hits/views for a particular object. | |||
Documentation: | |||
-------------- | |||
`<http://django-hitcount.rtfd.org>`_ | |||
Source Code: | |||
------------ | |||
`<https://github.com/thornomad/django-hitcount>`_ | |||
Issues | |||
------ | |||
Use the GitHub `issue tracker`_ for django-hitcount to submit bugs, issues, and feature requests. | |||
Changelog | |||
--------- | |||
`<http://django-hitcount.readthedocs.org/en/latest/changelog.html>`_ | |||
.. _issue tracker: https://github.com/thornomad/django-hitcount/issues | |||
@@ -0,0 +1 @@ | |||
pip |
@@ -0,0 +1,61 @@ | |||
Metadata-Version: 2.0 | |||
Name: django-hitcount | |||
Version: 1.3.0 | |||
Summary: Hit counting application for Django. | |||
Home-page: http://github.com/thornomad/django-hitcount | |||
Author: Damon Timm | |||
Author-email: damontimm@gmail.com | |||
License: BSD | |||
Description-Content-Type: UNKNOWN | |||
Platform: UNKNOWN | |||
Classifier: Development Status :: 4 - Beta | |||
Classifier: Environment :: Plugins | |||
Classifier: Framework :: Django | |||
Classifier: Intended Audience :: Developers | |||
Classifier: License :: OSI Approved :: BSD License | |||
Classifier: Programming Language :: Python | |||
Classifier: Topic :: Software Development :: Libraries :: Python Modules | |||
Classifier: Programming Language :: Python :: 2.6 | |||
Classifier: Programming Language :: Python :: 2.7 | |||
Classifier: Programming Language :: Python :: 3.2 | |||
Classifier: Programming Language :: Python :: 3.3 | |||
Classifier: Programming Language :: Python :: 3.4 | |||
django-hitcount | |||
=============== | |||
.. image:: https://travis-ci.org/thornomad/django-hitcount.svg?branch=master | |||
:target: https://travis-ci.org/thornomad/django-hitcount | |||
.. image:: https://coveralls.io/repos/thornomad/django-hitcount/badge.svg?branch=master | |||
:target: https://coveralls.io/r/thornomad/django-hitcount?branch=master | |||
.. image:: https://badge.fury.io/py/django-hitcount.svg | |||
:target: http://badge.fury.io/py/django-hitcount | |||
.. image:: https://requires.io/github/thornomad/django-hitcount/requirements.svg?branch=develop | |||
:target: https://requires.io/github/thornomad/django-hitcount/requirements/?branch=develop | |||
:alt: Requirements Status | |||
Basic app that allows you to track the number of hits/views for a particular object. | |||
Documentation: | |||
-------------- | |||
`<http://django-hitcount.rtfd.org>`_ | |||
Source Code: | |||
------------ | |||
`<https://github.com/thornomad/django-hitcount>`_ | |||
Issues | |||
------ | |||
Use the GitHub `issue tracker`_ for django-hitcount to submit bugs, issues, and feature requests. | |||
Changelog | |||
--------- | |||
`<http://django-hitcount.readthedocs.org/en/latest/changelog.html>`_ | |||
.. _issue tracker: https://github.com/thornomad/django-hitcount/issues | |||
@@ -0,0 +1,73 @@ | |||
django_hitcount-1.3.0.dist-info/DESCRIPTION.rst,sha256=qYNOokuJW5VaV917eTzyWlAD7GUFNfdb1Yuy39nxZnc,1179 | |||
django_hitcount-1.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 | |||
django_hitcount-1.3.0.dist-info/METADATA,sha256=ZfeWwRDcS89ApGq5n4Z0RkxfmHpNW7KbPI_qOVaVYGc,2029 | |||
django_hitcount-1.3.0.dist-info/RECORD,, | |||
django_hitcount-1.3.0.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110 | |||
django_hitcount-1.3.0.dist-info/metadata.json,sha256=nX26lMZ4TA-8fsrnzAHYW8HW6tJeIZiRkj-e1_qXHbM,950 | |||
django_hitcount-1.3.0.dist-info/top_level.txt,sha256=ciQsjrRx8k2LuKG2acan8SxCWV8ktTSJOo2s0eCnxOs,9 | |||
hitcount/__init__.py,sha256=l4S7L5a9RF9ngsnCumXsQgnhLbUDrLv75rVI1f37428,110 | |||
hitcount/__pycache__/__init__.cpython-36.pyc,, | |||
hitcount/__pycache__/admin.cpython-36.pyc,, | |||
hitcount/__pycache__/managers.cpython-36.pyc,, | |||
hitcount/__pycache__/models.cpython-36.pyc,, | |||
hitcount/__pycache__/signals.cpython-36.pyc,, | |||
hitcount/__pycache__/urls.cpython-36.pyc,, | |||
hitcount/__pycache__/utils.cpython-36.pyc,, | |||
hitcount/__pycache__/views.cpython-36.pyc,, | |||
hitcount/admin.py,sha256=YXinJSVGFwqDuVk0NSwu_PR4JNAkPpZrVvPqvp8Vir8,3517 | |||
hitcount/locale/ru/LC_MESSAGES/django.mo,sha256=4P3YSrZbBqm5vKywHU8OP1Yqi_uOr4_0k3ms9hNob3s,922 | |||
hitcount/locale/ru/LC_MESSAGES/django.po,sha256=fwKKdutWtaNjshRBuBHgQZS1jfPJ0wLlZPiIQ6l4BoU,1232 | |||
hitcount/management/.DS_Store,sha256=eJxOsIPnGpj5wPicqQp2GJotnEXWORC08JwewB8Z7zo,6148 | |||
hitcount/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | |||
hitcount/management/__init__.pyc,sha256=ci0qG4gwAh6InRZr7SC0Atn4Ncy4s69lxGTJ24xWL7Q,140 | |||
hitcount/management/__pycache__/__init__.cpython-34.pyc,sha256=xt5VdojOPPpEYLlZgrcNAUHpN-0LC2CVx6rqLxd14eo,153 | |||
hitcount/management/__pycache__/__init__.cpython-35.pyc,sha256=cJguE581coUsB_2F2LpCEr9P_UXhXydL56y-bKKXNhA,153 | |||
hitcount/management/__pycache__/__init__.cpython-36.pyc,sha256=yDorYsjqscWXfGjrO4Z0PBqSzyLzAaHhmc8CN4is9kM,153 | |||
hitcount/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | |||
hitcount/management/commands/__init__.pyc,sha256=bAJKmaMoifVBrisekW5ftymMLNpJugWfEkJ2668SIi0,166 | |||
hitcount/management/commands/__pycache__/__init__.cpython-34.pyc,sha256=mV0vGG856aEl-nuEL04INYA7iaRteb86yEjsilvwpSA,162 | |||
hitcount/management/commands/__pycache__/__init__.cpython-35.pyc,sha256=r4FrwdSELdLgB6nORVThqfr3ZyLXy0azSF5vbW_z6y4,162 | |||
hitcount/management/commands/__pycache__/__init__.cpython-36.pyc,sha256=M3hF87z_jcSVV9RHgAjVu977KGlnbtn8kS5o-SFySJ8,162 | |||
hitcount/management/commands/__pycache__/hitcount_cleanup.cpython-34-PYTEST.pyc,sha256=CVoedgQixzMUBq0ITb2Y977_rQhfyUuRn8ROB10UwTA,1717 | |||
hitcount/management/commands/__pycache__/hitcount_cleanup.cpython-34.pyc,sha256=hdrg5KZxFSk9OQ2XU97TIhoInj_IK5Fo_D4FLVM9Ct0,1600 | |||
hitcount/management/commands/__pycache__/hitcount_cleanup.cpython-35.pyc,sha256=ijfeqfAu_bTSjocAkhZiDU53zeVqtPa9U2A2-nGYXh0,1601 | |||
hitcount/management/commands/__pycache__/hitcount_cleanup.cpython-36.pyc,sha256=9DALGofFvZXxuReAdWHq-sCFmMrlvFfhGjECRnXl89k,1531 | |||
hitcount/management/commands/hitcount_cleanup.py,sha256=W8ubDD60KW-gDRDCqTiErhLB-VmvEIY69JlvZA60sCs,1004 | |||
hitcount/management/commands/hitcount_cleanup.pyc,sha256=kJ-jE3silVaQ5ThKJXq_8IH9cCXHV7wtvNgWEpFJd88,1458 | |||
hitcount/managers.py,sha256=sO7pNHNm3vbHmev7abW1ZHH8LG4swVXCWJhWRCD40S8,1609 | |||
hitcount/migrations/.DS_Store,sha256=knz5wSK6xyaXxFPjuov5ofdbkY45OUT1i5qHp4Hpb_0,6148 | |||
hitcount/migrations/0001_initial.py,sha256=Wi4WAnkCqoPZNC9K_mIQCBn7CRYUbWxZsCFe1oyl6ds,3808 | |||
hitcount/migrations/0001_initial.pyc,sha256=zUnc6TwI8t3lCBpYSS7PAqdGAEvIA4YlXu8X3tJIq8I,2886 | |||
hitcount/migrations/0002_index_ip_and_session.py,sha256=SD4cHYoj-kcOjfR8bOpzq6ZGGn75IAK-KFHNHHsUqNQ,778 | |||
hitcount/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | |||
hitcount/migrations/__init__.pyc,sha256=dYHNxrJCOUO4t3iyb8-1dHX51UIqEaEs5F1i6Xqk2eo,157 | |||
hitcount/migrations/__pycache__/0001_initial.cpython-34-PYTEST.pyc,sha256=pJYZWfZn294xkgGXKzynGghNM89LiAxsrBitOb8n_zU,2610 | |||
hitcount/migrations/__pycache__/0001_initial.cpython-34.pyc,sha256=pod5I9AJU1n_uiCo3af_g1Cr98ymvXWdQwdhee1WxzE,2504 | |||
hitcount/migrations/__pycache__/0001_initial.cpython-35.pyc,sha256=zz6vGM0RLLKWeE84UPIE4yGbZqdomjjl3ndZ8usRaH8,2525 | |||
hitcount/migrations/__pycache__/0001_initial.cpython-36.pyc,sha256=iyHZAAMqXfJ_bkROAoxUVmrWHMSdGSZ97qfc52OxE9I,2195 | |||
hitcount/migrations/__pycache__/0002_auto_20151002_0107.cpython-34.pyc,sha256=d9InaWAdfgzbNAmmehZuyjaUxewfXtmAiVJmzB15EE4,700 | |||
hitcount/migrations/__pycache__/0002_index_ip_and_session.cpython-35.pyc,sha256=GEFNCYxH7l2IJbq6aaFfpdXnmfZzymOLau-smEIP38E,888 | |||
hitcount/migrations/__pycache__/0002_index_ip_and_session.cpython-36.pyc,sha256=XJ0N1dFeDVmwR8CpSCAMOXtbQgGbSDypaV_cblq_9QM,787 | |||
hitcount/migrations/__pycache__/__init__.cpython-34.pyc,sha256=QnlwKINXZCxvEcxGbo6uz9hyeOP7_Sf22MWffdlz7zQ,153 | |||
hitcount/migrations/__pycache__/__init__.cpython-35.pyc,sha256=w-PkM7qV8_k4woZyC6AFVVHH0138x3RI5x73MgJ5QgY,153 | |||
hitcount/migrations/__pycache__/__init__.cpython-36.pyc,sha256=YZT2H038AusB1l8488oQ_iNqxUjPqBGetXivid8wTAA,153 | |||
hitcount/models.py,sha256=TCVZH8n2_7wV6rhJU0w4pj9xEGkqjl-Ho1bRW9feU1g,6452 | |||
hitcount/signals.py,sha256=dSAeXBqrfS3alXUyKaT_fTnLKwgpEYo8kSDrUitDYQY,160 | |||
hitcount/static/.DS_Store,sha256=rF6vsjXKJhA9tkZOK4dp1e3yiHhIpGTqeG_rErx9KOw,6148 | |||
hitcount/static/hitcount/hitcount-jquery.js,sha256=iuT92zgd8cZf3JPIOceugKRw7w8BVvMAb_o3896gnHY,1849 | |||
hitcount/static/hitcount/jquery.postcsrf.js,sha256=M7VJf_EO8XXUd9U07gUDndMcOL0a1ZL4DZdukdpTX6c,1696 | |||
hitcount/templatetags/.DS_Store,sha256=_bLXwPKHomfjzOqZXKLcl0Df7OVkpZwGLJEuHu46UVg,6148 | |||
hitcount/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | |||
hitcount/templatetags/__init__.pyc,sha256=lAeXmAsrGHVMuUKhPyjK_8583A_YDfDD-ameEtV0v4k,142 | |||
hitcount/templatetags/__pycache__/__init__.cpython-34.pyc,sha256=L-M9CXK_mJU_claLKAoKdLoyjl_9eykxvX29vZ1xjtU,138 | |||
hitcount/templatetags/__pycache__/__init__.cpython-35.pyc,sha256=RsqgNxtyYvbxiFcoZHhuMFBSRLcwud4a7D2xpk7n4rI,155 | |||
hitcount/templatetags/__pycache__/__init__.cpython-36.pyc,sha256=DhVijvXN2ym14mkK5ZquL_9FdSfKZA2Yr4z-30aT4_E,155 | |||
hitcount/templatetags/__pycache__/hitcount_tags.cpython-34-PYTEST.pyc,sha256=cGwj0P3A_4jOigoaG0BQPAYA8JXTOsaWG-jTzJHuJSg,10515 | |||
hitcount/templatetags/__pycache__/hitcount_tags.cpython-34.pyc,sha256=9PnFQoTv1PCwBBcqFaEOarhI62WNke2rFiEsYsUjCHo,10394 | |||
hitcount/templatetags/__pycache__/hitcount_tags.cpython-35.pyc,sha256=P3AcxUap_1zp_7I4plKRtth07vyiB3k5gQuIRSc4FW0,10448 | |||
hitcount/templatetags/__pycache__/hitcount_tags.cpython-36.pyc,sha256=JaI5iyvmXClChPx2pYnxXxvP-QoOB9ri6UekyZvOiqI,9869 | |||
hitcount/templatetags/hitcount_tags.py,sha256=ZhkDhw8ZNvXeRQ8oTb17C0-MTvxJISQ_k3jPup3zLlI,10322 | |||
hitcount/templatetags/hitcount_tags.pyc,sha256=lr1N24UpaEcgrOcrmU3RSL9sly4r7nkX8zlpmNY-nn4,9619 | |||
hitcount/urls.py,sha256=xBMnJP03XR9bEwhRB6PQmqhSV4rkoiqLs6wG6MIWhJA,255 | |||
hitcount/utils.py,sha256=6J-uHYKc7Orweo2ESVSoZqxy7Kvoc2Cr5ZjFTFWzGQs,1610 | |||
hitcount/views.py,sha256=cG9Tc7xUQT_PHKHi--wM9zQqdne9HmEl9Tq8uBNuAvE,7288 |
@@ -0,0 +1,6 @@ | |||
Wheel-Version: 1.0 | |||
Generator: bdist_wheel (0.30.0) | |||
Root-Is-Purelib: true | |||
Tag: py2-none-any | |||
Tag: py3-none-any | |||
@@ -0,0 +1 @@ | |||
{"classifiers": ["Development Status :: 4 - Beta", "Environment :: Plugins", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4"], "description_content_type": "UNKNOWN", "extensions": {"python.details": {"contacts": [{"email": "damontimm@gmail.com", "name": "Damon Timm", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/thornomad/django-hitcount"}}}, "generator": "bdist_wheel (0.30.0)", "license": "BSD", "metadata_version": "2.0", "name": "django-hitcount", "summary": "Hit counting application for Django.", "version": "1.3.0"} |
@@ -0,0 +1 @@ | |||
hitcount |
@@ -0,0 +1,5 @@ | |||
from __future__ import unicode_literals | |||
VERSION = (1, 3, 0) | |||
__version__ = '.'.join(str(i) for i in VERSION) |
@@ -0,0 +1,102 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from django.contrib import admin | |||
from django.core.exceptions import PermissionDenied | |||
from django.utils.translation import ugettext_lazy as _ | |||
from .models import Hit, HitCount, BlacklistIP, BlacklistUserAgent | |||
class HitAdmin(admin.ModelAdmin): | |||
list_display = ('created', 'user', 'ip', 'user_agent', 'hitcount') | |||
search_fields = ('ip', 'user_agent') | |||
date_hierarchy = 'created' | |||
actions = ['blacklist_ips', | |||
'blacklist_user_agents', | |||
'blacklist_delete_ips', | |||
'blacklist_delete_user_agents', | |||
'delete_queryset', | |||
] | |||
def __init__(self, *args, **kwargs): | |||
super(HitAdmin, self).__init__(*args, **kwargs) | |||
self.list_display_links = None | |||
def has_add_permission(self, request): | |||
return False | |||
def get_actions(self, request): | |||
actions = super(HitAdmin, self).get_actions(request) | |||
if 'delete_selected' in actions: | |||
del actions['delete_selected'] | |||
return actions | |||
def blacklist_ips(self, request, queryset): | |||
for obj in queryset: | |||
ip, created = BlacklistIP.objects.get_or_create(ip=obj.ip) | |||
if created: | |||
ip.save() | |||
msg = _("Successfully blacklisted %d IPs") % queryset.count() | |||
self.message_user(request, msg) | |||
blacklist_ips.short_description = _("Blacklist selected IP addresses") | |||
def blacklist_user_agents(self, request, queryset): | |||
for obj in queryset: | |||
ua, created = BlacklistUserAgent.objects.get_or_create( | |||
user_agent=obj.user_agent) | |||
if created: | |||
ua.save() | |||
msg = _("Successfully blacklisted %d User Agents") % queryset.count() | |||
self.message_user(request, msg) | |||
blacklist_user_agents.short_description = _("Blacklist selected User Agents") | |||
def blacklist_delete_ips(self, request, queryset): | |||
self.blacklist_ips(request, queryset) | |||
self.delete_queryset(request, queryset) | |||
blacklist_delete_ips.short_description = _( | |||
"Delete selected hits and blacklist related IP addresses") | |||
def blacklist_delete_user_agents(self, request, queryset): | |||
self.blacklist_user_agents(request, queryset) | |||
self.delete_queryset(request, queryset) | |||
blacklist_delete_user_agents.short_description = _( | |||
"Delete selected hits and blacklist related User Agents") | |||
def delete_queryset(self, request, queryset): | |||
if not self.has_delete_permission(request): | |||
raise PermissionDenied | |||
else: | |||
if queryset.count() == 1: | |||
msg = "1 hit was" | |||
else: | |||
msg = "%s hits were" % queryset.count() | |||
for obj in queryset.iterator(): | |||
obj.delete() # calling it this way to get custom delete() method | |||
self.message_user(request, "%s successfully deleted." % msg) | |||
delete_queryset.short_description = _("Delete selected hits") | |||
admin.site.register(Hit, HitAdmin) | |||
class HitCountAdmin(admin.ModelAdmin): | |||
list_display = ('content_object', 'hits', 'modified') | |||
fields = ('hits',) | |||
def has_add_permission(self, request): | |||
return False | |||
admin.site.register(HitCount, HitCountAdmin) | |||
class BlacklistIPAdmin(admin.ModelAdmin): | |||
pass | |||
admin.site.register(BlacklistIP, BlacklistIPAdmin) | |||
class BlacklistUserAgentAdmin(admin.ModelAdmin): | |||
pass | |||
admin.site.register(BlacklistUserAgent, BlacklistUserAgentAdmin) |
@@ -0,0 +1,51 @@ | |||
# SOME DESCRIPTIVE TITLE. | |||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | |||
# This file is distributed under the same license as the PACKAGE package. | |||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | |||
# | |||
msgid "" | |||
msgstr "" | |||
"Project-Id-Version: \n" | |||
"Report-Msgid-Bugs-To: \n" | |||
"POT-Creation-Date: 2013-07-08 12:52+0700\n" | |||
"PO-Revision-Date: 2013-07-08 13:31+0700\n" | |||
"Last-Translator: Basil Shubin <basil.shubin@gmail.com>\n" | |||
"Language-Team: <RU@li.org>\n" | |||
"MIME-Version: 1.0\n" | |||
"Content-Type: text/plain; charset=UTF-8\n" | |||
"Content-Transfer-Encoding: 8bit\n" | |||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | |||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | |||
"X-Generator: Poedit 1.5.4\n" | |||
#: models.py:87 | |||
msgid "hit count" | |||
msgstr "счетчик просмотров" | |||
#: models.py:88 | |||
msgid "hit counts" | |||
msgstr "просмотры" | |||
#: models.py:144 | |||
msgid "hit" | |||
msgstr "хит" | |||
#: models.py:145 | |||
msgid "hits" | |||
msgstr "хиты" | |||
#: models.py:183 | |||
msgid "Blacklisted IP" | |||
msgstr "бан" | |||
#: models.py:184 | |||
msgid "Blacklisted IPs" | |||
msgstr "черный список (IP)" | |||
#: models.py:195 | |||
msgid "Blacklisted User Agent" | |||
msgstr "бан" | |||
#: models.py:196 | |||
msgid "Blacklisted User Agents" | |||
msgstr "черный список (UA)" |
@@ -0,0 +1,32 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from datetime import timedelta | |||
from django.conf import settings | |||
from django.utils import timezone | |||
try: | |||
from django.core.management.base import BaseCommand | |||
except ImportError: | |||
from django.core.management.base import NoArgsCommand as BaseCommand | |||
from hitcount.models import Hit | |||
class Command(BaseCommand): | |||
help = "Can be run as a cronjob or directly to clean out old Hits objects from the database." | |||
def __init__(self, *args, **kwargs): | |||
super(Command, self).__init__(*args, **kwargs) | |||
def handle(self, *args, **kwargs): | |||
self.handle_noargs() | |||
def handle_noargs(self, **options): | |||
grace = getattr(settings, 'HITCOUNT_KEEP_HIT_IN_DATABASE', {'days': 30}) | |||
period = timezone.now() - timedelta(**grace) | |||
qs = Hit.objects.filter(created__lt=period) | |||
number_removed = qs.count() | |||
qs.delete() | |||
self.stdout.write('Successfully removed %s Hits' % number_removed) |
@@ -0,0 +1,45 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from datetime import timedelta | |||
from django.db import models | |||
from django.conf import settings | |||
from django.utils import timezone | |||
from django.contrib.contenttypes.models import ContentType | |||
class HitCountManager(models.Manager): | |||
def get_for_object(self, obj): | |||
ctype = ContentType.objects.get_for_model(obj) | |||
hit_count, created = self.get_or_create( | |||
content_type=ctype, object_pk=obj.pk) | |||
return hit_count | |||
class HitManager(models.Manager): | |||
def filter_active(self, *args, **kwargs): | |||
""" | |||
Return only the 'active' hits. | |||
How you count a hit/view will depend on personal choice: Should the | |||
same user/visitor *ever* be counted twice? After a week, or a month, | |||
or a year, should their view be counted again? | |||
The defaulf is to consider a visitor's hit still 'active' if they | |||
return within a the last seven days.. After that the hit | |||
will be counted again. So if one person visits once a week for a year, | |||
they will add 52 hits to a given object. | |||
Change how long the expiration is by adding to settings.py: | |||
HITCOUNT_KEEP_HIT_ACTIVE = {'days' : 30, 'minutes' : 30} | |||
Accepts days, seconds, microseconds, milliseconds, minutes, | |||
hours, and weeks. It's creating a datetime.timedelta object. | |||
""" | |||
grace = getattr(settings, 'HITCOUNT_KEEP_HIT_ACTIVE', {'days': 7}) | |||
period = timezone.now() - timedelta(**grace) | |||
return self.filter(created__gte=period).filter(*args, **kwargs) |
@@ -0,0 +1,94 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from django.db import models, migrations | |||
from django.conf import settings | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | |||
('contenttypes', '0001_initial'), | |||
] | |||
operations = [ | |||
migrations.CreateModel( | |||
name='BlacklistIP', | |||
fields=[ | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | |||
('ip', models.CharField(unique=True, max_length=40)), | |||
], | |||
options={ | |||
'db_table': 'hitcount_blacklist_ip', | |||
'verbose_name': 'Blacklisted IP', | |||
'verbose_name_plural': 'Blacklisted IPs', | |||
}, | |||
bases=(models.Model,), | |||
), | |||
migrations.CreateModel( | |||
name='BlacklistUserAgent', | |||
fields=[ | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | |||
('user_agent', models.CharField(unique=True, max_length=255)), | |||
], | |||
options={ | |||
'db_table': 'hitcount_blacklist_user_agent', | |||
'verbose_name': 'Blacklisted User Agent', | |||
'verbose_name_plural': 'Blacklisted User Agents', | |||
}, | |||
bases=(models.Model,), | |||
), | |||
migrations.CreateModel( | |||
name='Hit', | |||
fields=[ | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | |||
('created', models.DateTimeField(auto_now_add=True, db_index=True)), | |||
('ip', models.CharField(max_length=40, editable=False)), | |||
('session', models.CharField(max_length=40, editable=False)), | |||
('user_agent', models.CharField(max_length=255, editable=False)), | |||
], | |||
options={ | |||
'ordering': ('-created',), | |||
'get_latest_by': 'created', | |||
'verbose_name': 'hit', | |||
'verbose_name_plural': 'hits', | |||
}, | |||
bases=(models.Model,), | |||
), | |||
migrations.CreateModel( | |||
name='HitCount', | |||
fields=[ | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | |||
('hits', models.PositiveIntegerField(default=0)), | |||
('modified', models.DateTimeField(auto_now=True)), | |||
('object_pk', models.PositiveIntegerField(verbose_name='object ID')), | |||
('content_type', models.ForeignKey(related_name='content_type_set_for_hitcount', | |||
to='contenttypes.ContentType', on_delete=models.CASCADE)), | |||
], | |||
options={ | |||
'get_latest_by': 'modified', | |||
'ordering': ('-hits',), | |||
'verbose_name_plural': 'hit counts', | |||
'db_table': 'hitcount_hit_count', | |||
'verbose_name': 'hit count', | |||
}, | |||
bases=(models.Model,), | |||
), | |||
migrations.AlterUniqueTogether( | |||
name='hitcount', | |||
unique_together=set([('content_type', 'object_pk')]), | |||
), | |||
migrations.AddField( | |||
model_name='hit', | |||
name='hitcount', | |||
field=models.ForeignKey(editable=False, to='hitcount.HitCount', on_delete=models.CASCADE), | |||
preserve_default=True, | |||
), | |||
migrations.AddField( | |||
model_name='hit', | |||
name='user', | |||
field=models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), | |||
preserve_default=True, | |||
), | |||
] |
@@ -0,0 +1,29 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from django.db import migrations, models | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('hitcount', '0001_initial'), | |||
] | |||
operations = [ | |||
migrations.AlterField( | |||
model_name='hit', | |||
name='ip', | |||
field=models.CharField(max_length=40, db_index=True, editable=False), | |||
), | |||
migrations.AlterField( | |||
model_name='hit', | |||
name='session', | |||
field=models.CharField(max_length=40, db_index=True, editable=False), | |||
), | |||
migrations.AlterField( | |||
model_name='hitcount', | |||
name='object_pk', | |||
field=models.PositiveIntegerField(verbose_name='object ID'), | |||
), | |||
] |
@@ -0,0 +1,197 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from datetime import timedelta | |||
from django.db import models | |||
from django.conf import settings | |||
from django.db.models import F | |||
from django.utils import timezone | |||
from django.dispatch import receiver | |||
from django.utils.encoding import python_2_unicode_compatible | |||
from django.contrib.contenttypes.fields import GenericForeignKey | |||
from django.contrib.contenttypes.models import ContentType | |||
from django.utils.translation import ugettext_lazy as _ | |||
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') | |||
from .managers import HitCountManager, HitManager | |||
from .signals import delete_hit_count | |||
@receiver(delete_hit_count) | |||
def delete_hit_count_handler(sender, instance, save_hitcount=False, **kwargs): | |||
""" | |||
Custom callback for the Hit.delete() method. | |||
Hit.delete(): removes the hit from the associated HitCount object. | |||
Hit.delete(save_hitcount=True): preserves the hit for the associated | |||
HitCount object. | |||
""" | |||
if not save_hitcount: | |||
instance.hitcount.decrease() | |||
@python_2_unicode_compatible | |||
class HitCount(models.Model): | |||
""" | |||
Model that stores the hit totals for any content object. | |||
""" | |||
hits = models.PositiveIntegerField(default=0) | |||
modified = models.DateTimeField(auto_now=True) | |||
content_type = models.ForeignKey( | |||
ContentType, related_name="content_type_set_for_%(class)s", on_delete=models.CASCADE) | |||
object_pk = models.PositiveIntegerField('object ID') | |||
content_object = GenericForeignKey('content_type', 'object_pk') | |||
objects = HitCountManager() | |||
class Meta: | |||
ordering = ('-hits',) | |||
get_latest_by = "modified" | |||
verbose_name = _("hit count") | |||
verbose_name_plural = _("hit counts") | |||
unique_together = ("content_type", "object_pk") | |||
db_table = "hitcount_hit_count" | |||
def __str__(self): | |||
return '%s' % self.content_object | |||
def increase(self): | |||
self.hits = F('hits') + 1 | |||
self.save() | |||
def decrease(self): | |||
self.hits = F('hits') - 1 | |||
self.save() | |||
def hits_in_last(self, **kwargs): | |||
""" | |||
Returns hit count for an object during a given time period. | |||
This will only work for as long as hits are saved in the Hit database. | |||
If you are purging your database after 45 days, for example, that means | |||
that asking for hits in the last 60 days will return an incorrect | |||
number as that the longest period it can search will be 45 days. | |||
For example: hits_in_last(days=7). | |||
Accepts days, seconds, microseconds, milliseconds, minutes, | |||
hours, and weeks. It's creating a datetime.timedelta object. | |||
""" | |||
assert kwargs, "Must provide at least one timedelta arg (eg, days=1)" | |||
period = timezone.now() - timedelta(**kwargs) | |||
return self.hit_set.filter(created__gte=period).count() | |||
# def get_content_object_url(self): | |||
# """ | |||
# Django has this in its contrib.comments.model file -- seems worth | |||
# implementing though it may take a couple steps. | |||
# | |||
# """ | |||
# pass | |||
@python_2_unicode_compatible | |||
class Hit(models.Model): | |||
""" | |||
Model captures a single Hit by a visitor. | |||
None of the fields are editable because they are all dynamically created. | |||
Browsing the Hit list in the Admin will allow one to blacklist both | |||
IP addresses as well as User Agents. Blacklisting simply causes those | |||
hits to not be counted or recorded. | |||
Depending on how long you set the HITCOUNT_KEEP_HIT_ACTIVE, and how long | |||
you want to be able to use `HitCount.hits_in_last(days=30)` you can choose | |||
to clean up your Hit table by using the management `hitcount_cleanup` | |||
management command. | |||
""" | |||
created = models.DateTimeField(editable=False, auto_now_add=True, db_index=True) | |||
ip = models.CharField(max_length=40, editable=False, db_index=True) | |||
session = models.CharField(max_length=40, editable=False, db_index=True) | |||
user_agent = models.CharField(max_length=255, editable=False) | |||
user = models.ForeignKey(AUTH_USER_MODEL, null=True, editable=False, on_delete=models.CASCADE) | |||
hitcount = models.ForeignKey(HitCount, editable=False, on_delete=models.CASCADE) | |||
objects = HitManager() | |||
class Meta: | |||
ordering = ('-created',) | |||
get_latest_by = 'created' | |||
verbose_name = _("hit") | |||
verbose_name_plural = _("hits") | |||
def __str__(self): | |||
return 'Hit: %s' % self.pk | |||
def save(self, *args, **kwargs): | |||
""" | |||
The first time the object is created and saved, we increment | |||
the associated HitCount object by one. The opposite applies | |||
if the Hit is deleted. | |||
""" | |||
if self.pk is None: | |||
self.hitcount.increase() | |||
super(Hit, self).save(*args, **kwargs) | |||
def delete(self, save_hitcount=False): | |||
""" | |||
If a Hit is deleted and save_hitcount=True, it will preserve the | |||
HitCount object's total. However, under normal circumstances, a | |||
delete() will trigger a subtraction from the HitCount object's total. | |||
NOTE: This doesn't work at all during a queryset.delete(). | |||
""" | |||
delete_hit_count.send( | |||
sender=self, instance=self, save_hitcount=save_hitcount) | |||
super(Hit, self).delete() | |||
@python_2_unicode_compatible | |||
class BlacklistIP(models.Model): | |||
ip = models.CharField(max_length=40, unique=True) | |||
class Meta: | |||
db_table = "hitcount_blacklist_ip" | |||
verbose_name = _("Blacklisted IP") | |||
verbose_name_plural = _("Blacklisted IPs") | |||
def __str__(self): | |||
return '%s' % self.ip | |||
@python_2_unicode_compatible | |||
class BlacklistUserAgent(models.Model): | |||
user_agent = models.CharField(max_length=255, unique=True) | |||
class Meta: | |||
db_table = "hitcount_blacklist_user_agent" | |||
verbose_name = _("Blacklisted User Agent") | |||
verbose_name_plural = _("Blacklisted User Agents") | |||
def __str__(self): | |||
return '%s' % self.user_agent | |||
class HitCountMixin(object): | |||
""" | |||
HitCountMixin provides an easy way to add a `hit_count` property to your | |||
model that will return the related HitCount object. | |||
""" | |||
@property | |||
def hit_count(self): | |||
ctype = ContentType.objects.get_for_model(self.__class__) | |||
hit_count, created = HitCount.objects.get_or_create( | |||
content_type=ctype, object_pk=self.pk) | |||
return hit_count |
@@ -0,0 +1,5 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from django.dispatch import Signal | |||
delete_hit_count = Signal(providing_args=['save_hitcount']) |
@@ -0,0 +1,60 @@ | |||
$(document).ready(function() { | |||
/** | |||
* https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax | |||
* | |||
* Remember you will need to ensure csrf tokens by adding: | |||
* @ensure_csrf_cookie to your views that require this javascript | |||
* | |||
* Also, you will probably want to include this with your other sitewide | |||
* javascript files ... this is just an example. | |||
*/ | |||
if ( typeof hitcountJS === 'undefined' ) { | |||
// since this is loaded on every page only do something | |||
// if a hit is going to be counted | |||
return; | |||
} | |||
var hitcountPK = hitcountJS['hitcountPK']; | |||
var hitcountURL = hitcountJS['hitcountURL']; | |||
var csrftoken = getCookie('csrftoken'); | |||
$.ajaxSetup({ | |||
beforeSend: function(xhr, settings) { | |||
if (!csrfSafeMethod(settings.type) && !this.crossDomain) { | |||
xhr.setRequestHeader("X-CSRFToken", csrftoken); | |||
} | |||
} | |||
}); | |||
$.post( hitcountURL, { "hitcountPK" : hitcountPK }, | |||
function(data, status) { | |||
console.log(data); // just so you can see the response | |||
if (data.status == 'error') { | |||
// do something for error? | |||
} | |||
}, 'json'); | |||
}); | |||
function getCookie(name) { | |||
var cookieValue = null; | |||
if (document.cookie && document.cookie != '') { | |||
var cookies = document.cookie.split(';'); | |||
for (var i = 0; i < cookies.length; i++) { | |||
var cookie = jQuery.trim(cookies[i]); | |||
// Does this cookie string begin with the name we want? | |||
if (cookie.substring(0, name.length + 1) == (name + '=')) { | |||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); | |||
break; | |||
} | |||
} | |||
} | |||
return cookieValue; | |||
} | |||
function csrfSafeMethod(method) { | |||
// these HTTP methods do not require CSRF protection | |||
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); | |||
} |
@@ -0,0 +1,59 @@ | |||
/** | |||
* Wrapper for jQuery's $.post() that retrieves the CSRF token from the browser | |||
* cookie and sets then sets "X-CSRFToken" header in one fell swoop. | |||
* | |||
* Based on the example code given at the Django docs: | |||
* https://docs.djangoproject.com/en/1.9/ref/csrf/#ajax | |||
* | |||
* Use as you would $.post(). | |||
*/ | |||
(function($) { | |||
$.postCSRF = function(url, data, callback, type) { | |||
function csrfSafeMethod(method) { | |||
// these HTTP methods do not require CSRF protection | |||
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); | |||
} | |||
function getCookie(name) { | |||
var cookieValue = null; | |||
if (document.cookie && document.cookie !== '') { | |||
var cookies = document.cookie.split(';'); | |||
for (var i = 0; i < cookies.length; i++) { | |||
var cookie = jQuery.trim(cookies[i]); | |||
// Does this cookie string begin with the name we want? | |||
if (cookie.substring(0, name.length + 1) == (name + '=')) { | |||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); | |||
break; | |||
} | |||
} | |||
} | |||
return cookieValue; | |||
} | |||
var csrftoken = getCookie('csrftoken'); | |||
// shift arguments if data argument was omitted | |||
if ($.isFunction(data)) { | |||
type = type || callback; | |||
callback = data; | |||
data = undefined; | |||
} | |||
return $.ajax(jQuery.extend({ | |||
url: url, | |||
type: "POST", | |||
dataType: type, | |||
data: data, | |||
success: callback, | |||
beforeSend: function(xhr, settings) { | |||
if (!csrfSafeMethod(settings.type) && !this.crossDomain) { | |||
xhr.setRequestHeader("X-CSRFToken", csrftoken); | |||
} | |||
} | |||
}, jQuery.isPlainObject(url) && url)); | |||
}; | |||
}(jQuery)); |
@@ -0,0 +1,319 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from collections import namedtuple | |||
from django import template | |||
from django.contrib.contenttypes.models import ContentType | |||
try: | |||
from django.core.urlresolvers import reverse | |||
except ImportError: | |||
from django.urls import reverse | |||
from hitcount.models import HitCount | |||
register = template.Library() | |||
def get_hit_count_from_obj_variable(context, obj_variable, tag_name): | |||
""" | |||
Helper function to return a HitCount for a given template object variable. | |||
Raises TemplateSyntaxError if the passed object variable cannot be parsed. | |||
""" | |||
error_to_raise = template.TemplateSyntaxError( | |||
"'%(a)s' requires a valid individual model variable " | |||
"in the form of '%(a)s for [model_obj]'.\n" | |||
"Got: %(b)s" % {'a': tag_name, 'b': obj_variable} | |||
) | |||
try: | |||
obj = obj_variable.resolve(context) | |||
except template.VariableDoesNotExist: | |||
raise error_to_raise | |||
try: | |||
ctype = ContentType.objects.get_for_model(obj) | |||
except AttributeError: | |||
raise error_to_raise | |||
hit_count, created = HitCount.objects.get_or_create( | |||
content_type=ctype, object_pk=obj.pk) | |||
return hit_count | |||
def return_period_from_string(arg): | |||
""" | |||
Takes a string such as "days=1,seconds=30" and strips the quotes | |||
and returns a dictionary with the key/value pairs | |||
""" | |||
period = {} | |||
if arg[0] == '"' and arg[-1] == '"': | |||
opt = arg[1:-1] # remove quotes | |||
else: | |||
opt = arg | |||
for o in opt.split(","): | |||
key, value = o.split("=") | |||
period[str(key)] = int(value) | |||
return period | |||
class GetHitCount(template.Node): | |||
def handle_token(cls, parser, token): | |||
args = token.contents.split() | |||
# {% get_hit_count for [obj] %} | |||
if len(args) == 3 and args[1] == 'for': | |||
return cls(obj_as_str=args[2]) | |||
# {% get_hit_count for [obj] as [var] %} | |||
elif len(args) == 5 and args[1] == 'for' and args[3] == 'as': | |||
return cls(obj_as_str=args[2], | |||
as_varname=args[4],) | |||
# {% get_hit_count for [obj] within ["days=1,minutes=30"] %} | |||
elif len(args) == 5 and args[1] == 'for' and args[3] == 'within': | |||
return cls(obj_as_str=args[2], | |||
period=return_period_from_string(args[4])) | |||
# {% get_hit_count for [obj] within ["days=1,minutes=30"] as [var] %} | |||
elif len(args) == 7 and args[1] == 'for' and \ | |||
args[3] == 'within' and args[5] == 'as': | |||
return cls(obj_as_str=args[2], | |||
as_varname=args[6], | |||
period=return_period_from_string(args[4])) | |||
else: # TODO - should there be more troubleshooting prior to bailing? | |||
raise template.TemplateSyntaxError( | |||
"'get_hit_count' requires " | |||
"'for [object] in [period] as [var]' (got %r)" % args | |||
) | |||
handle_token = classmethod(handle_token) | |||
def __init__(self, obj_as_str, as_varname=None, period=None): | |||
self.obj_variable = template.Variable(obj_as_str) | |||
self.as_varname = as_varname | |||
self.period = period | |||
def render(self, context): | |||
hit_count = get_hit_count_from_obj_variable(context, self.obj_variable, 'get_hit_count') | |||
if self.period: # if user sets a time period, use it | |||
try: | |||
hits = hit_count.hits_in_last(**self.period) | |||
except TypeError: | |||
raise template.TemplateSyntaxError( | |||
"'get_hit_count for [obj] within [timedelta]' requires " | |||
"a valid comma separated list of timedelta arguments. " | |||
"For example, ['days=5,hours=6']. " | |||
"Got these instead: %s" % self.period | |||
) | |||
else: | |||
hits = hit_count.hits | |||
if self.as_varname: # if user gives us a variable to return | |||
context[self.as_varname] = str(hits) | |||
return '' | |||
else: | |||
return str(hits) | |||
def get_hit_count(parser, token): | |||
""" | |||
Returns hit counts for an object. | |||
- Return total hits for an object: | |||
{% get_hit_count for [object] %} | |||
- Get total hits for an object as a specified variable: | |||
{% get_hit_count for [object] as [var] %} | |||
- Get total hits for an object over a certain time period: | |||
{% get_hit_count for [object] within ["days=1,minutes=30"] %} | |||
- Get total hits for an object over a certain time period as a variable: | |||
{% get_hit_count for [object] within ["days=1,minutes=30"] as [var] %} | |||
The time arguments need to follow datetime.timedelta's limitations: | |||
Accepts days, seconds, microseconds, milliseconds, minutes, | |||
hours, and weeks. | |||
""" | |||
return GetHitCount.handle_token(parser, token) | |||
register.tag('get_hit_count', get_hit_count) | |||
class WriteHitCountJavascriptVariables(template.Node): | |||
def handle_token(cls, parser, token): | |||
args = token.contents.split() | |||
if len(args) == 3 and args[1] == 'for': | |||
return cls(obj_variable=args[2]) | |||
else: | |||
raise template.TemplateSyntaxError( | |||
'insert_hit_count_js_variables requires this syntax: ' | |||
'"insert_hit_count_js_variables for [object]"\n' | |||
'Got: %s' % ' '.join(str(i) for i in args) | |||
) | |||
handle_token = classmethod(handle_token) | |||
def __init__(self, obj_variable): | |||
self.obj_variable = template.Variable(obj_variable) | |||
def render(self, context): | |||
hit_count = get_hit_count_from_obj_variable(context, self.obj_variable, 'insert_hit_count_js_variables') | |||
js = '<script type="text/javascript">\n' + \ | |||
"var hitcountJS = {" + \ | |||
"hitcountPK : '" + str(hit_count.pk) + "'," + \ | |||
"hitcountURL : '" + str(reverse('hitcount:hit_ajax')) + "'};" + \ | |||
"\n</script>" | |||
return js | |||
def insert_hit_count_js_variables(parser, token): | |||
""" | |||
Injects JavaScript global variables into your template. These variables | |||
can be used in your JavaScript files to send the correctly mapped HitCount | |||
ID to the server (see: hitcount-jquery.js for an example). | |||
{% insert_hit_count_js_variables for [object] %} | |||
""" | |||
return WriteHitCountJavascriptVariables.handle_token(parser, token) | |||
register.tag('insert_hit_count_js_variables', insert_hit_count_js_variables) | |||
class GetHitCountJavascriptVariables(template.Node): | |||
def handle_token(cls, parser, token): | |||
args = token.contents.split() | |||
if len(args) == 5 and args[1] == 'for' and args[3] == 'as': | |||
return cls(obj_variable=args[2], as_varname=args[4]) | |||
else: | |||
raise template.TemplateSyntaxError( | |||
'get_hit_count_js_variables requires this syntax: ' | |||
'"get_hit_count_js_variables for [object] as [var_name]."\n' | |||
'Got: %s' % ' '.join(str(i) for i in args) | |||
) | |||
handle_token = classmethod(handle_token) | |||
def __init__(self, obj_variable, as_varname): | |||
self.obj_variable = template.Variable(obj_variable) | |||
self.as_varname = as_varname | |||
def render(self, context): | |||
HitcountVariables = namedtuple('HitcountVariables', 'pk ajax_url hits') | |||
hit_count = get_hit_count_from_obj_variable(context, self.obj_variable, 'get_hit_count_js_variables') | |||
context[self.as_varname] = HitcountVariables( | |||
hit_count.pk, str(reverse('hitcount:hit_ajax')), str(hit_count.hits)) | |||
return '' | |||
def get_hit_count_js_variables(parser, token): | |||
""" | |||
Injects JavaScript global variables into your template. These variables | |||
can be used in your JavaScript files to send the correctly mapped HitCount | |||
ID to the server (see: hitcount-jquery.js for an example). | |||
{% get_hit_count_js_variables for [object] as [var_name] %} | |||
Will provide two variables: | |||
[var_name].pk = the hitcount pk to be sent via JavaScript | |||
[var_name].ajax_url = the relative url to post the ajax request to | |||
""" | |||
return GetHitCountJavascriptVariables.handle_token(parser, token) | |||
register.tag('get_hit_count_js_variables', get_hit_count_js_variables) | |||
class WriteHitCountJavascript(template.Node): | |||
JS_TEMPLATE = """ | |||
<script type="text/javascript"> | |||
//<![CDATA[ | |||
jQuery(document).ready(function($) { | |||
$.postCSRF("%s", { | |||
hitcountPK: "%s" | |||
}); | |||
}); | |||
//]]> | |||
</script> | |||
""" | |||
JS_TEMPLATE_DEBUG = """ | |||
<script type="text/javascript"> | |||
//<![CDATA[ | |||
jQuery(document).ready(function($) { | |||
$.postCSRF("%s", { | |||
hitcountPK: "%s" | |||
}).done(function(data) { | |||
console.log('django-hitcount: AJAX POST succeeded.'); | |||
console.log(data); | |||
}).fail(function(data) { | |||
console.log('django-hitcount: AJAX POST failed.'); | |||
console.log(data); | |||
}); | |||
}); | |||
//]]> | |||
</script> | |||
""" | |||
def handle_token(cls, parser, token): | |||
args = token.contents.split() | |||
if len(args) == 3 and args[1] == 'for': | |||
return cls(obj_variable=args[2], debug=False) | |||
elif len(args) == 4 and args[1] == 'for' and args[3] == 'debug': | |||
return cls(obj_variable=args[2], debug=True) | |||
else: | |||
raise template.TemplateSyntaxError( | |||
'insert_hit_count_js requires this syntax: ' | |||
'"insert_hit_count_js for [object]"\n' | |||
'"insert_hit_count_js for [object] debug"' | |||
'Got: %s' % ' '.join(str(i) for i in args) | |||
) | |||
handle_token = classmethod(handle_token) | |||
def __init__(self, obj_variable, debug): | |||
self.obj_variable = template.Variable(obj_variable) | |||
self.debug = debug | |||
def render(self, context): | |||
hit_count = get_hit_count_from_obj_variable( | |||
context, | |||
self.obj_variable, | |||
'insert_hit_count_js' | |||
) | |||
template = self.JS_TEMPLATE_DEBUG if self.debug else self.JS_TEMPLATE | |||
return template % (str(reverse('hitcount:hit_ajax')), str(hit_count.pk)) | |||
def insert_hit_count_js(parser, token): | |||
""" | |||
Injects the JavaScript into your template that works with jquery.postcsrf.js. | |||
{% insert_hit_count_js_variables for [object] %} | |||
""" | |||
return WriteHitCountJavascript.handle_token(parser, token) | |||
register.tag('insert_hit_count_js', insert_hit_count_js) |
@@ -0,0 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from django.conf.urls import url | |||
from hitcount.views import HitCountJSONView | |||
app_name = 'hitcount' | |||
urlpatterns = [ | |||
url(r'^hit/ajax/$', HitCountJSONView.as_view(), name='hit_ajax'), | |||
] |
@@ -0,0 +1,47 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
import re | |||
import warnings | |||
# this is not intended to be an all-knowing IP address regex | |||
IP_RE = re.compile('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') | |||
def get_ip(request): | |||
""" | |||
Retrieves the remote IP address from the request data. If the user is | |||
behind a proxy, they may have a comma-separated list of IP addresses, so | |||
we need to account for that. In such a case, only the first IP in the | |||
list will be retrieved. Also, some hosts that use a proxy will put the | |||
REMOTE_ADDR into HTTP_X_FORWARDED_FOR. This will handle pulling back the | |||
IP from the proper place. | |||
**NOTE** This function was taken from django-tracking (MIT LICENSE) | |||
http://code.google.com/p/django-tracking/ | |||
""" | |||
# if neither header contain a value, just use local loopback | |||
ip_address = request.META.get('HTTP_X_FORWARDED_FOR', | |||
request.META.get('REMOTE_ADDR', '127.0.0.1')) | |||
if ip_address: | |||
# make sure we have one and only one IP | |||
try: | |||
ip_address = IP_RE.match(ip_address) | |||
if ip_address: | |||
ip_address = ip_address.group(0) | |||
else: | |||
# no IP, probably from some dirty proxy or other device | |||
# throw in some bogus IP | |||
ip_address = '10.0.0.1' | |||
except IndexError: | |||
pass | |||
return ip_address | |||
class RemovedInHitCount13Warning(DeprecationWarning): | |||
pass | |||
# enable warnings by default for our deprecated | |||
warnings.simplefilter("default", RemovedInHitCount13Warning) |
@@ -0,0 +1,189 @@ | |||
# -*- coding: utf-8 -*- | |||
import warnings | |||
from collections import namedtuple | |||
from django.http import Http404, JsonResponse, HttpResponseBadRequest | |||
from django.conf import settings | |||
from django.views.generic import View, DetailView | |||
from hitcount.utils import get_ip | |||
from hitcount.models import Hit, HitCount, BlacklistIP, BlacklistUserAgent | |||
from hitcount.utils import RemovedInHitCount13Warning | |||
class HitCountMixin(object): | |||
""" | |||
Mixin to evaluate a HttpRequest and a HitCount and determine whether or not | |||
the HitCount should be incremented and the Hit recorded. | |||
""" | |||
@classmethod | |||
def hit_count(self, request, hitcount): | |||
""" | |||
Called with a HttpRequest and HitCount object it will return a | |||
namedtuple: | |||
UpdateHitCountResponse(hit_counted=Boolean, hit_message='Message'). | |||
`hit_counted` will be True if the hit was counted and False if it was | |||
not. `'hit_message` will indicate by what means the Hit was either | |||
counted or ignored. | |||
""" | |||
UpdateHitCountResponse = namedtuple( | |||
'UpdateHitCountResponse', 'hit_counted hit_message') | |||
# as of Django 1.8.4 empty sessions are not being saved | |||
# https://code.djangoproject.com/ticket/25489 | |||
if request.session.session_key is None: | |||
request.session.save() | |||
user = request.user | |||
try: | |||
is_authenticated_user = user.is_authenticated() | |||
except: | |||
is_authenticated_user = user.is_authenticated | |||
session_key = request.session.session_key | |||
ip = get_ip(request) | |||
user_agent = request.META.get('HTTP_USER_AGENT', '')[:255] | |||
hits_per_ip_limit = getattr(settings, 'HITCOUNT_HITS_PER_IP_LIMIT', 0) | |||
exclude_user_group = getattr(settings, 'HITCOUNT_EXCLUDE_USER_GROUP', None) | |||
# first, check our request against the IP blacklist | |||
if BlacklistIP.objects.filter(ip__exact=ip): | |||
return UpdateHitCountResponse( | |||
False, 'Not counted: user IP has been blacklisted') | |||
# second, check our request against the user agent blacklist | |||
if BlacklistUserAgent.objects.filter(user_agent__exact=user_agent): | |||
return UpdateHitCountResponse( | |||
False, 'Not counted: user agent has been blacklisted') | |||
# third, see if we are excluding a specific user group or not | |||
if exclude_user_group and is_authenticated_user: | |||
if user.groups.filter(name__in=exclude_user_group): | |||
return UpdateHitCountResponse( | |||
False, 'Not counted: user excluded by group') | |||
# eliminated first three possible exclusions, now on to checking our database of | |||
# active hits to see if we should count another one | |||
# start with a fresh active query set (HITCOUNT_KEEP_HIT_ACTIVE) | |||
qs = Hit.objects.filter_active() | |||
# check limit on hits from a unique ip address (HITCOUNT_HITS_PER_IP_LIMIT) | |||
if hits_per_ip_limit: | |||
if qs.filter(ip__exact=ip).count() >= hits_per_ip_limit: | |||
return UpdateHitCountResponse( | |||
False, 'Not counted: hits per IP address limit reached') | |||
# create a generic Hit object with request data | |||
hit = Hit(session=session_key, hitcount=hitcount, ip=get_ip(request), | |||
user_agent=request.META.get('HTTP_USER_AGENT', '')[:255],) | |||
# first, use a user's authentication to see if they made an earlier hit | |||
if is_authenticated_user: | |||
if not qs.filter(user=user, hitcount=hitcount): | |||
hit.user = user # associate this hit with a user | |||
hit.save() | |||
response = UpdateHitCountResponse( | |||
True, 'Hit counted: user authentication') | |||
else: | |||
response = UpdateHitCountResponse( | |||
False, 'Not counted: authenticated user has active hit') | |||
# if not authenticated, see if we have a repeat session | |||
else: | |||
if not qs.filter(session=session_key, hitcount=hitcount): | |||
hit.save() | |||
response = UpdateHitCountResponse( | |||
True, 'Hit counted: session key') | |||
else: | |||
response = UpdateHitCountResponse( | |||
False, 'Not counted: session key has active hit') | |||
return response | |||
class HitCountJSONView(View, HitCountMixin): | |||
""" | |||
JSON response view to handle HitCount POST. | |||
""" | |||
def dispatch(self, request, *args, **kwargs): | |||
if not request.is_ajax(): | |||
raise Http404() | |||
return super(HitCountJSONView, self).dispatch(request, *args, **kwargs) | |||
def get(self, request, *args, **kwargs): | |||
msg = "Hits counted via POST only." | |||
return JsonResponse({'success': False, 'error_message': msg}) | |||
def post(self, request, *args, **kwargs): | |||
hitcount_pk = request.POST.get('hitcountPK') | |||
try: | |||
hitcount = HitCount.objects.get(pk=hitcount_pk) | |||
except: | |||
return HttpResponseBadRequest("HitCount object_pk not working") | |||
hit_count_response = self.hit_count(request, hitcount) | |||
return JsonResponse(hit_count_response._asdict()) | |||
class HitCountDetailView(DetailView, HitCountMixin): | |||
""" | |||
HitCountDetailView provides an inherited DetailView that will inject the | |||
template context with a `hitcount` variable giving you the number of | |||
Hits for an object without using a template tag. | |||
Optionally, by setting `count_hit = True` you can also do the business of | |||
counting the Hit for this object (in lieu of using JavaScript). It will | |||
then further inject the response from the attempt to count the Hit into | |||
the template context. | |||
""" | |||
count_hit = False | |||
def get_context_data(self, **kwargs): | |||
context = super(HitCountDetailView, self).get_context_data(**kwargs) | |||
if self.object: | |||
hit_count = HitCount.objects.get_for_object(self.object) | |||
hits = hit_count.hits | |||
context['hitcount'] = {'pk': hit_count.pk} | |||
if self.count_hit: | |||
hit_count_response = self.hit_count(self.request, hit_count) | |||
if hit_count_response.hit_counted: | |||
hits = hits + 1 | |||
context['hitcount']['hit_counted'] = hit_count_response.hit_counted | |||
context['hitcount']['hit_message'] = hit_count_response.hit_message | |||
context['hitcount']['total_hits'] = hits | |||
return context | |||
def _update_hit_count(request, hitcount): | |||
""" | |||
Deprecated in 1.2. Use hitcount.views.Hit CountMixin.hit_count() instead. | |||
""" | |||
warnings.warn( | |||
"hitcount.views._update_hit_count is deprecated. " | |||
"Use hitcount.views.HitCountMixin.hit_count() instead.", | |||
RemovedInHitCount13Warning | |||
) | |||
return HitCountMixin.hit_count(request, hitcount) | |||
def update_hit_count_ajax(request, *args, **kwargs): | |||
""" | |||
Deprecated in 1.2. Use hitcount.views.HitCountJSONView instead. | |||
""" | |||
warnings.warn( | |||
"hitcount.views.update_hit_count_ajax is deprecated. " | |||
"Use hitcount.views.HitCountJSONView instead.", | |||
RemovedInHitCount13Warning | |||
) | |||
view = HitCountJSONView.as_view() | |||
return view(request, *args, **kwargs) |