Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 152 additions & 48 deletions website/templates/includes/page_stats.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{% load custom_tags %}
{% get_current_template as current_template %}
{% get_page_views current_template 30 as page_views %}
{% get_current_url_path as current_url_path %}
{% get_page_views current_url_path 30 as page_views %}
{% get_page_votes current_template as upvotes %}
{% get_page_votes current_template "downvote" as downvotes %}
<div id="pageStatsContainer"
class="fixed bottom-0 left-4 z-50 transition-all duration-300 transform translate-y-[calc(100%-40px)] hover:translate-y-0 group">
<!-- Chart Icon and Handle -->
<!-- Chart Icon and Handle here -->
<div class="flex justify-center items-center h-10 w-16 mx-auto bg-white rounded-t-lg shadow-md cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-[#e74c3c]"
Expand All @@ -16,11 +17,11 @@
</svg>
</div>
<!-- Stats Content -->
<div class="bg-white shadow-lg rounded-lg p-3 w-64">
<div class="text-sm font-medium text-gray-700 mb-2">Page Statistics (Last 30 Days)</div>
<div class="bg-white shadow-lg rounded-lg p-3 w-72">
<div class="text-sm font-medium text-gray-700 mb-2">Page Views (Last 30 Days)</div>
<!-- Bar Chart -->
<div class="mb-3">
<canvas id="pageViewsChart" height="100"></canvas>
<canvas id="pageViewsChart" height="120"></canvas>
</div>
<!-- Vote Buttons -->
<div class="flex justify-between items-center">
Expand All @@ -36,6 +37,9 @@
</button>
<span id="upvoteCount" class="text-sm">{{ upvotes }}</span>
</div>
<div class="text-xs text-center text-gray-500">
Total: <span id="totalViews">0</span> Views
</div>
<div class="flex items-center">
<button id="downvoteBtn"
class="text-gray-600 hover:text-[#e74c3c] focus:outline-none mr-2">
Expand Down Expand Up @@ -63,46 +67,101 @@

const ctx = document.getElementById('pageViewsChart').getContext('2d');

// Initialize default empty data structure
let viewsByDate = {};

// Safely parse the page views data from Django template
// Safely parse the page views data from Django template
let pageViewsData;
try {


// Try to parse the data, ensuring it's an array of numbers
pageViewsData = JSON.parse('{{ page_views|safe|escapejs }}');
console.log(pageViewsData);

// Ensure the data consists of numbers
pageViewsData = pageViewsData.map(count => Number(count) || 0);

} catch (e) {
console.error('Failed to parse page views data:', e);
pageViewsData = Array(30).fill(0);
}
// Generate labels for the last 30 days (day numbers only)
const labels = [];
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
labels.push(date.getDate());
try {
// Get the raw JSON string and ensure it's properly escaped/formatted
const rawData = '{{ page_views|escapejs }}';

// Parse the JSON data only if it's not empty
if (rawData && rawData.trim() !== '') {
viewsByDate = JSON.parse(rawData);
console.log('Successfully parsed view data:', viewsByDate);
} else {
console.warn('Empty page views data received');
}
} catch (e) {
console.error('Failed to parse page views data:', e);
}

// Ensure viewsByDate is an object, not null or undefined
if (!viewsByDate || typeof viewsByDate !== 'object') {
console.warn('Invalid view data structure, creating empty object');
viewsByDate = {};
}

// Sort the dates chronologically
const sortedDates = Object.keys(viewsByDate).sort();

// Extract data in chronological order
const dateLabels = [];
const pageViewsData = [];

// Format dates for display
const shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// Process each date if we have any
if (sortedDates.length > 0) {
for (const dateStr of sortedDates) {
try {
// Parse the date from the YYYY-MM-DD format
const dateParts = dateStr.split('-');
if (dateParts.length !== 3) {
console.warn(`Invalid date format: ${dateStr}`);
continue;
}

const year = parseInt(dateParts[0], 10);
const month = parseInt(dateParts[1], 10) - 1; // 0-based month
const day = parseInt(dateParts[2], 10);

// Validate date parts
if (isNaN(year) || isNaN(month) || isNaN(day) || month < 0 || month > 11 || day < 1 || day > 31) {
console.warn(`Invalid date parts: ${year}-${month+1}-${day}`);
continue;
}

// Format for display
dateLabels.push(`${shortMonthNames[month]} ${day}`);

// Add view count for this date (ensure it's a number)
const viewCount = parseInt(viewsByDate[dateStr], 10);
pageViewsData.push(isNaN(viewCount) ? 0 : viewCount);
} catch (e) {
console.warn(`Error processing date ${dateStr}:`, e);
// Skip this date if there's an error
}
}
} else {
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
dateLabels.push(`${shortMonthNames[date.getMonth()]} ${date.getDate()}`);
pageViewsData.push(0);
}
}

// Calculate total views - ensure we're only summing numbers
const totalViews = pageViewsData.reduce((sum, count) => sum + (isNaN(count) ? 0 : count), 0);
document.getElementById('totalViews').textContent = totalViews;

// Create the chart
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
labels: dateLabels,
datasets: [{
label: 'Unique Views',
label: 'Views',
data: pageViewsData,
backgroundColor: 'rgba(231, 76, 60, 0.7)',
borderColor: '#e74c3c',
borderWidth: 1,
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.8
barPercentage: 0.8,
categoryPercentage: 0.9
}]
},
options: {
Expand All @@ -116,12 +175,28 @@
enabled: true,
callbacks: {
title: function(tooltipItems) {
const date = new Date(today);
date.setDate(date.getDate() - (29 - tooltipItems[0].dataIndex));
return date.toLocaleDateString();
const index = tooltipItems[0].dataIndex;
if (sortedDates.length > 0 && index < sortedDates.length) {
try {
const dateStr = sortedDates[index];
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
} catch (e) {
console.warn('Error formatting tooltip date:', e);
}
}
// Fallback to just showing the label if we can't format the date
return dateLabels[index] || 'Unknown Date';
},
label: function(context) {
const count = context.raw || 0;
const count = isNaN(context.raw) ? 0 : context.raw;
return count + ' view' + (count !== 1 ? 's' : '');
}
}
Expand All @@ -133,9 +208,13 @@
display: false
},
ticks: {
maxRotation: 0,
maxRotation: 45,
minRotation: 45,
autoSkip: true,
maxTicksLimit: 10
maxTicksLimit: 15,
font: {
size: 9
}
}
},
y: {
Expand All @@ -144,7 +223,14 @@
color: 'rgba(200, 200, 200, 0.2)'
},
ticks: {
precision: 0
precision: 0,
stepSize: 1,
// Ensure we only show integer values
callback: function(value) {
if (value % 1 === 0) {
return value;
}
}
}
}
}
Expand All @@ -161,17 +247,35 @@
const downvoteBtn = document.getElementById('downvoteBtn');
const upvoteCount = document.getElementById('upvoteCount');
const downvoteCount = document.getElementById('downvoteCount');
const currentTemplate = '{{ current_template }}';
const currentUrlPath = '{{ current_url_path }}';

// Toggle container on click of the handle
const handleElement = pageStatsContainer.querySelector('.flex.justify-center');
if (handleElement) {
handleElement.addEventListener('click', function(e) {
e.preventDefault();
if (pageStatsContainer.classList.contains('translate-y-0')) {
pageStatsContainer.classList.remove('translate-y-0');
pageStatsContainer.classList.add('translate-y-[calc(100%-40px)]');
} else {
pageStatsContainer.classList.remove('translate-y-[calc(100%-40px)]');
pageStatsContainer.classList.add('translate-y-0');
}
});
}

// Add touch support for mobile devices
pageStatsContainer.addEventListener('touchstart', function(e) {
e.preventDefault();
if (pageStatsContainer.classList.contains('translate-y-0')) {
pageStatsContainer.classList.remove('translate-y-0');
pageStatsContainer.classList.add('translate-y-[calc(100%-40px)]');
} else {
pageStatsContainer.classList.remove('translate-y-[calc(100%-40px)]');
pageStatsContainer.classList.add('translate-y-0');
// Only handle touch events on the handle element
if (e.target.closest('.flex.justify-center')) {
e.preventDefault();
if (pageStatsContainer.classList.contains('translate-y-0')) {
pageStatsContainer.classList.remove('translate-y-0');
pageStatsContainer.classList.add('translate-y-[calc(100%-40px)]');
} else {
pageStatsContainer.classList.remove('translate-y-[calc(100%-40px)]');
pageStatsContainer.classList.add('translate-y-0');
}
}
});

Expand All @@ -196,7 +300,7 @@
// Vote submission handler
function submitVote(voteType) {
const formData = new FormData();
formData.append('template_name', currentTemplate);
formData.append('url_path', currentUrlPath);
formData.append('vote_type', voteType);

fetch('{% url "page_vote" %}', {
Expand Down
50 changes: 38 additions & 12 deletions website/templatetags/custom_tags.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import random
from datetime import timedelta

Expand Down Expand Up @@ -66,43 +67,68 @@ def multiply(value, arg):
return 0


@register.simple_tag(takes_context=True)
def get_current_url_path(context):
"""
Returns the current URL path from the request
"""
request = context.get("request")
if request:
return request.path
return None


@register.simple_tag(takes_context=True)
def get_current_template(context):
"""
Returns the current template name from the template context
"""

if hasattr(context, "template") and hasattr(context.template, "name"):
return context.template.name
return None


@register.simple_tag
def get_page_views(template_name, days=30):
def get_page_views(url_path, days=30):
"""
Returns the page view data for the last N days for a specific template
Returns page view data for the last N days for a specific URL path,
as a JSON dictionary with dates as keys and view counts as values.
"""
# Get the date range
end_date = timezone.now()
start_date = end_date - timedelta(days=days)

# Query the IP table for views of this page
daily_views = (
IP.objects.filter(path__contains=template_name, created__gte=start_date, created__lte=end_date)
IP.objects.filter(path__contains=url_path, created__gte=start_date, created__lte=end_date)
.values("created__date")
.annotate(count=models.Count("id"))
.annotate(total_views=models.Sum("count"))
.order_by("created__date")
)

# Convert to a list of counts
view_counts = [0] * days
date_map = {(start_date + timedelta(days=i)).date(): i for i in range(days)}
# Create a dictionary with dates as keys and view counts as values
view_counts_dict = {}

for entry in daily_views:
day_index = date_map.get(entry["created__date"])
if day_index is not None:
view_counts[day_index] = entry["count"]
# First, initialize the dictionary with zeros for all dates in the range
for i in range(days):
current_date = (start_date + timedelta(days=i)).date()
formatted_date = current_date.strftime("%Y-%m-%d")
view_counts_dict[formatted_date] = 0

return view_counts
# Then populate with actual view data where available
for entry in daily_views:
try:
date_str = entry["created__date"].strftime("%Y-%m-%d")
# Ensure view count is an integer
count = int(entry["total_views"])
view_counts_dict[date_str] = count
except (AttributeError, ValueError, TypeError) as e:
# In case of any error, just skip this entry but don't crash
continue

# Return as JSON string
return json.dumps(view_counts_dict)


@register.simple_tag
Expand Down