{% extends 'base.html' %} {% block content %} | |||||
{% extends 'base.html' %}{% block content %} | |||||
{% load hitcount_tags %} | |||||
<div class="post"> | <div class="post"> | ||||
{% if post.published_date %} | {% if post.published_date %} | ||||
<div class="date"> | <div class="date"> | ||||
{{ post.published_date }} | {{ post.published_date }} | ||||
<h1>{{ post.title }}</h1> | <h1>{{ post.title }}</h1> | ||||
<p>{{ post.text|linebreaksbr }}</p> | <p>{{ post.text|linebreaksbr }}</p> | ||||
<p> | <p> | ||||
Tags: | |||||
{% for tag in post.tags.all %} | {% for tag in post.tags.all %} | ||||
<a href="{% url 'blog_search_list_view' %}">{{ tag.name }}, </a> | <a href="{% url 'blog_search_list_view' %}">{{ tag.name }}, </a> | ||||
{% endfor %} | {% endfor %} | ||||
<p> | <p> | ||||
Autor: {{ post.author }} | Autor: {{ post.author }} | ||||
</p> | </p> | ||||
{% if user.is_staff %} | |||||
<p> | |||||
{% get_hit_count for post %} Benutzer haben diesen Post bereits gelesen! | |||||
</p> | |||||
{% endif %} | |||||
</div> | </div> | ||||
{% endblock %} | {% endblock %} |
from post_office.models import EmailTemplate | from post_office.models import EmailTemplate | ||||
from post_office import mail | from post_office import mail | ||||
from hitcount.models import HitCount | |||||
from hitcount.views import HitCountMixin | |||||
import logging | import logging | ||||
@login_required | @login_required | ||||
def post_detail(request, pk): | def post_detail(request, pk): | ||||
post = get_object_or_404(Post, pk=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 | @login_required | ||||
def tag_cloud(request): | def tag_cloud(request): | ||||
return render(request, 'tag_cloud.html', {}) | 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( | mail.send( | ||||
'kleinhenz.e@gmail.com', # List of email addresses also accepted | 'kleinhenz.e@gmail.com', # List of email addresses also accepted | ||||
'esther.kleinhenz@web.de', | 'esther.kleinhenz@web.de', | ||||
template='weekly-update', | |||||
context={'name': 'alice'}, | |||||
subject='My email', | |||||
message='Hi there!', | |||||
html_message='Hi <strong>there</strong>!', | |||||
) | ) |
'taggit_templatetags2', | 'taggit_templatetags2', | ||||
'kombu.transport.django', | 'kombu.transport.django', | ||||
'post_office', | 'post_office', | ||||
'hitcount', | |||||
] | ] | ||||
MIDDLEWARE = [ | MIDDLEWARE = [ |
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 | |||||
pip |
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 | |||||
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 |
Wheel-Version: 1.0 | |||||
Generator: bdist_wheel (0.30.0) | |||||
Root-Is-Purelib: true | |||||
Tag: py2-none-any | |||||
Tag: py3-none-any | |||||
{"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"} |
hitcount |
from __future__ import unicode_literals | |||||
VERSION = (1, 3, 0) | |||||
__version__ = '.'.join(str(i) for i in VERSION) |
# -*- 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) |
# 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)" |
# -*- 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) |
# -*- 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) |
# -*- 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, | |||||
), | |||||
] |
# -*- 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'), | |||||
), | |||||
] |
# -*- 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 |
# -*- coding: utf-8 -*- | |||||
from __future__ import unicode_literals | |||||
from django.dispatch import Signal | |||||
delete_hit_count = Signal(providing_args=['save_hitcount']) |
$(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)); | |||||
} |
/** | |||||
* 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)); |
# -*- 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) |
# -*- 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'), | |||||
] |
# -*- 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) |
# -*- 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) |