diff --git a/analytics/__init__.py b/analytics/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/analytics/__init__.py diff --git a/analytics/admin.py b/analytics/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/analytics/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/analytics/apps.py b/analytics/apps.py new file mode 100644 index 0000000..2a2cf57 --- /dev/null +++ b/analytics/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AnalyticsConfig(AppConfig): + name = 'analytics' diff --git a/analytics/migrations/__init__.py b/analytics/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/analytics/migrations/__init__.py diff --git a/analytics/models.py b/analytics/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/analytics/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/analytics/static/.sass-cache/9bdb779ec82a4a96f72be09b83b7c997addd0129/general.sassc b/analytics/static/.sass-cache/9bdb779ec82a4a96f72be09b83b7c997addd0129/general.sassc new file mode 100644 index 0000000..9ac09a5 Binary files /dev/null and b/analytics/static/.sass-cache/9bdb779ec82a4a96f72be09b83b7c997addd0129/general.sassc differ diff --git a/analytics/static/analytics/general.css b/analytics/static/analytics/general.css new file mode 100644 index 0000000..35d1ca7 --- /dev/null +++ b/analytics/static/analytics/general.css @@ -0,0 +1,43 @@ +.report-body { + border: 1px solid #3ebeb4; + border-radius: 20px; + padding: 10px; } + +#general-report-header { + height: 10%; + display: block; } + #general-report-header h3 { + color: #009688; } + #general-report-header ul { + float: right; + display: inline-flex; + width: 50%; + height: 15%; } + #general-report-header ul li { + margin-right: 2%; + width: 50%; + color: white; } + #general-report-header ul li div.selected { + background-color: #4dcfbd; + width: 100%; + text-align: center; + border-radius: 20px; } + #general-report-header ul li div { + background-color: #0f8a9a; + width: 100%; + text-align: center; + border-radius: 20px; } + +#most-used-tags-header { + background: linear-gradient(to right, #0e8999, #6bf0ce); + height: 40px; + border-radius: 10px; + color: #ffffff; } + +div.tag-cloud { + width: 10%; + border-radius: 25px; + color: #ffffff; + background-color: #52b7bd; } + +/*# sourceMappingURL=general.css.map */ diff --git a/analytics/static/analytics/general.css.map b/analytics/static/analytics/general.css.map new file mode 100644 index 0000000..b659a69 --- /dev/null +++ b/analytics/static/analytics/general.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,YAAY;EACX,MAAM,EAAE,iBAAiB;EACzB,aAAa,EAAE,IAAI;EACnB,OAAO,EAAE,IAAI;;AAGd,sBAAsB;EACrB,MAAM,EAAE,GAAG;EACX,OAAO,EAAE,KAAK;EACd,yBAAE;IACD,KAAK,EAAE,OAAO;EAEf,yBAAE;IACD,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE,WAAW;IACpB,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IAEX,4BAAE;MACD,YAAY,EAAE,EAAE;MAChB,KAAK,EAAE,GAAG;MACV,KAAK,EAAE,KAAK;MAEZ,yCAAY;QACX,gBAAgB,EAAE,OAAO;QACzB,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,MAAM;QAClB,aAAa,EAAE,IAAI;MAEpB,gCAAG;QACF,gBAAgB,EAAE,OAAO;QACzB,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,MAAM;QAClB,aAAa,EAAE,IAAI;;AAIvB,sBAAsB;EACrB,UAAU,EAAE,2CAA2C;EACvD,MAAM,EAAE,IAAI;EACZ,aAAa,EAAE,IAAI;EACnB,KAAK,EAAE,OAAO;;AAEf,aAAa;EACZ,KAAK,EAAE,GAAG;EACV,aAAa,EAAE,IAAI;EACnB,KAAK,EAAE,OAAO;EACd,gBAAgB,EAAE,OAAO", +"sources": ["general.sass"], +"names": [], +"file": "general.css" +} \ No newline at end of file diff --git a/analytics/static/analytics/general.sass b/analytics/static/analytics/general.sass new file mode 100644 index 0000000..7b1c963 --- /dev/null +++ b/analytics/static/analytics/general.sass @@ -0,0 +1,48 @@ +.report-body + border: 1px solid #3ebeb4 + border-radius: 20px + padding: 10px + + +#general-report-header + height: 10% + display: block + h3 + color: #009688 + + ul + float: right + display: inline-flex + width: 50% + height: 15% + + li + margin-right: 2% + width: 50% + color: white + + div.selected + background-color: #4dcfbd + width: 100% + text-align: center + border-radius: 20px + + div + background-color: #0f8a9a + width: 100% + text-align: center + border-radius: 20px + + + +#most-used-tags-header + background: linear-gradient(to right, #0e8999, #6bf0ce) + height: 40px + border-radius: 10px + color: #ffffff + +div.tag-cloud + width: 10% + border-radius: 25px + color: #ffffff + background-color: #52b7bd \ No newline at end of file diff --git a/analytics/static/analytics/js/charts.js b/analytics/static/analytics/js/charts.js new file mode 100644 index 0000000..17ad291 --- /dev/null +++ b/analytics/static/analytics/js/charts.js @@ -0,0 +1,378 @@ +/* + HOME CHARTS +*/ + +//Adding this code by @jshanley to create a filter to look for the last child + +d3.selection.prototype.first = function() { + return d3.select(this[0]); +}; +d3.selection.prototype.last = function() { + var last = this.size() - 1; + return d3.select(this[last]); +}; + +var charts = { + build_resource: function(url){ + $.get(url, function(dataset){ + + + var width = 600; + var height = 300; + var padding = 30; + var radius = Math.min(width, height) / 2 - padding; + + var color = d3.scaleOrdinal(d3.schemeCategory20); + var new_div = d3.select(".carousel-inner").append("div").attr("class","item"); + var svg = new_div.append("svg").attr("width", width).attr("height", height) + .style("margin","auto") + .style("display","block") + .append('g') + .attr('transform', 'translate(' + (width / 2) + + ',' + (height / 2 + padding ) + ')'); + + + + var donutInner = 20; + var arc = d3.arc() + .innerRadius(radius - donutInner) + .outerRadius(radius); + + svg.append("text") + .attr("text-anchor", "middle") + .attr("x",0 ) + .attr("y", -height/2 ) + .style("font-size", "30px") + .text("Recursos mais utilizados") + .attr("fill", "#003333") + .style("font-weight", "bold") + .style("font-style", "italic"); + + + var pie = d3.pie() + .value(function(d) { return d[1]; }) + .sort(null); + + + + var path = svg.selectAll('path') + .data(pie(dataset)) + .enter() + .append('path') + .attr('d', arc) + .attr('fill', function(d, i) { + return color(i); + }); + var labelArc = d3.arc() + .outerRadius(radius - donutInner + 30) + .innerRadius(radius + 20 ); + + //Adding tooltips to each part of the pie chart. + svg.selectAll("text.pie-tooltip") + .data(pie(dataset)) + .enter() + .append("text") + .attr("id", function(d){ + return d.data[0]; + }) + .attr("class","pie-tooltip") + .attr('fill',"#172121") + .attr("transform", function(d) { + c = labelArc.centroid(d); + return "translate(" + (c[0]*1.0 - 20) +"," + c[1]*0.8 + ")"; + }) + .attr("dy", ".25em") + .text(function(d) { return d.data[0] +'('+ d.data[1] +')'; }); + + + + }) // end of the get method + }, + + build_bubble_user: function(url){ + $.get(url, function(dataset){ + var width = 600; + var height = 300; + + + function min(){ + min = 100000000000; + for(var i = 0; i < dataset.length; i++){ + if (dataset[i]['count'] < min){ + min = dataset[i]['count']; + } + } + + return min + } + + function max(){ + max = 0; + for(var i = 0; i < dataset.length; i++){ + if (dataset[i]['count'] > max){ + max = dataset[i]['count']; + } + } + + return max + } + + + + var color = d3.scaleOrdinal(d3.schemeCategory20); + //adding new div to carousel-inner div + var new_div = d3.select(".carousel-inner").append("div").attr("class","item"); + var radiusScale = d3.scaleSqrt().domain([min(), max()]).range([10,50]); + var svg = new_div.append("svg").attr("width", width).attr("height", height) + .style("margin","auto") + .style("display","block") + .append('g') + .attr("transform", "translate(0,0)") + .attr("width", width) + .attr("height", height); + + //adding svg title + + svg.append("text") + .attr("text-anchor", "middle") + .attr("x", width/2 ) + .attr("y", 30) + .style("font-size", "30px") + .text("Usuários mais ativos no Amadeus") + .attr("fill", "#003333") + .style("font-weight", "bold") + .style("font-style", "italic"); + + var simulation = d3.forceSimulation() + .force("x", d3.forceX(width/2).strength(0.05)) + .force("y", d3.forceY(height/2).strength(0.05)) + .force("collide", d3.forceCollide(function(d){ + return radiusScale(d['count']); + })); + + //First I create as many groups as datapoints so + //it can hold all other objects (circle, texts, images) + var groups = svg.selectAll('.users-item') + .data(dataset) + .enter() + .append("g") + .attr("class",".user-dot"); + + //Create circles to be drawn + var circles = groups + .append('circle') + .attr("r", function(d){ + return radiusScale(d['count']); + }) + + .attr("fill", function(d){ + return 'url('+'#'+'user_'+d['user_id']+')'; + }); + + + + //Add texts to show user names + groups.append("text") + .text(function(d){ + return d['user'] +'('+ d['count'] + ')'; + }).attr("fill", "#FFFFFF") + .attr("id", function(d){ + return "user_tooltip_"+d['user_id']; + }).style("display", "none"); + + + groups.on("mouseover", function(d){ + $("#"+"user_tooltip_"+d['user_id']).show(); + }); + + + groups.on("mouseout", function(d){ + $("#"+"user_tooltip_"+d['user_id']).hide(); + }); + + var defs = groups.append('svg:defs'); + + //Attching images to bubbles + defs.append("svg:pattern") + .attr("id", function(d){ + return "user_"+d['user_id']; + }) + .attr("width", function(d){ + return radiusScale(d['count']); + }) + .attr("height", function(d){ + return radiusScale(d['count']); + }) + .append("svg:image") + .attr("xlink:href", function(d){ + return d['image']; + }) + .attr("width",function(d){ + return radiusScale(d['count'])*2; + }) + .attr("height", function(d){ + return radiusScale(d['count'])*2; + }) + .attr("x", 0) + .attr("y", 0); + + + + //simulation + simulation.nodes(dataset) + .on('tick', ticked); //so all data points are attached to it + + function ticked(){ + groups.attr("transform", function(d){ + return "translate(" + d.x + "," + d.y + ")"; + }) + } + }); + + + }, + + most_accessed_subjects: function(url){ + $.get(url, function(dataset){ + var width = 800; + var height = 300; + + var new_div = d3.select(".carousel-inner").append("div").attr("class","item"); + + var svg = new_div.append("svg").attr("width", "100%").attr("height", height) + .style("margin","auto") + .style("display","block"); + + var barPadding = 2 + var bottomPadding = 15; + var marginLeft = 170; //Margin Left for all bars inside the graph + + var yScale = d3.scaleLinear() + .domain([0, d3.max(dataset, function(d) { return d.count; })]) + .range([bottomPadding, 260]); + + var rects = svg.selectAll("rect") + .data(dataset) + .enter() + .append("g"); + + rects.append("rect") + .attr("x", function(d, i){ + return i * (width / dataset.length ) + barPadding + marginLeft ; + }) + .attr("y", function(d){ + return height - yScale(d.count) - bottomPadding; + }) + .attr("width", + width / dataset.length - barPadding + ) + .attr("height", function(d){ + return yScale(d.count); + }); + + + var tooltipDiv = new_div.append("div").attr("class","bar-tip"); + + rects.on("mouseover", function(d, i){ + $(this).attr("fill", "red"); + $("#accessed_subject_"+i).show(); //So the tooltip is hidden + tooltipDiv.transition() + .duration(200) + .style("opacity", .9); + tooltipDiv.html("

" + d.name + "( " + d.count + " )" +"

") + .style("left", (i * (width / dataset.length ) + barPadding + marginLeft) + "px") + .style("top", (height - yScale(d.count) - bottomPadding) + "px"); + }); + + rects.on("mouseout", function(d, i ){ + $(this).attr("fill", "black"); + + $("#accessed_subject_"+i).hide(); //So the tooltip shows up + + + tooltipDiv.transition() + .duration(500) + .style("opacity", 0); + + }); + + + + //Styling title + svg.append("text") + .attr("x", width/2) + .attr("y", 30) + .attr("text-anchor", "middle") + .style("font-size", "30px") + .text("Subjects mais acessados") + .attr("fill", "#003333") + .style("font-weight", "bold") + .style("font-style", "italic"); + + }); + }, + most_used_tags: function(url){ + $.get(url, function(dataset){ + //get most used tags across all amadeus + var width = 800; + var height = 300; + + function min(){ + min = 100000000000; + for(var i = 0; i < dataset.length; i++){ + if (dataset[i]['count'] < min){ + min = dataset[i]['count']; + } + } + + return min + } + + function max(){ + max = 0; + for(var i = 0; i < dataset.length; i++){ + if (dataset[i]['count'] > max){ + max = dataset[i]['count']; + } + } + + return max + } + + console.log(dataset); + + var container_div = d3.select("#most-used-tags-body"); + var svg = container_div.append("svg").attr("width", "100%").attr("height", height) + .style("margin","auto") + .style("display","block") + .style("background","#ddf8e7") + .append('g') + .attr("transform", "translate(0,0)") + .attr("width", width) + .attr("height", height); + + + + var radiusScale = d3.scaleSqrt().domain([min(), max()]).range([10,50]); + var tag_cloud = svg.selectAll('.tag-cloud-div') + .data(dataset) + .enter() + .append('g') + .attr("class", "data-container"); + + + var tag_divs = tag_cloud + .append('div') + .attr('class', 'tag-cloud') + .attr('text', function(d){ + return d['name']; + }); + + }); + } +} + +charts.most_used_tags('/analytics/most_used_tags'); +//charts.build_resource('/topics/count_resources/'); +//charts.build_bubble_user('/users/get_users_log/'); +//charts.most_accessed_subjects('/subjects/most_accessed_subjects'); \ No newline at end of file diff --git a/analytics/templates/analytics/general.html b/analytics/templates/analytics/general.html new file mode 100644 index 0000000..9dcebd6 --- /dev/null +++ b/analytics/templates/analytics/general.html @@ -0,0 +1,76 @@ +{% extends 'base.html' %} + +{% load static i18n pagination %} +{% load django_bootstrap_breadcrumbs %} +{% block style %} + +{% endblock style %} + +{% block javascript %} + +{% endblock javascript %} + +{% block breadcrumbs %} + {{ block.super }} + + {% trans 'Analytics General' as general %} + + {% breadcrumb general 'dashboard:view_general' %} +{% endblock %} + + +{% block content %} +
+ +
+ +
+
+

{% trans "Amadeus Report" %}

+ + +
+ +
+ +
+
+

+ {% trans "most used tags" %} +

+
+
+ +
+
+
+
+
+
+
+ +
+
+ + + + +{% endblock content %} \ No newline at end of file diff --git a/analytics/tests.py b/analytics/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/analytics/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/analytics/tests/__init__.py b/analytics/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/analytics/tests/__init__.py diff --git a/analytics/tests/test_api.py b/analytics/tests/test_api.py new file mode 100644 index 0000000..3e82598 --- /dev/null +++ b/analytics/tests/test_api.py @@ -0,0 +1,45 @@ +from django.test import TestCase, Client, override_settings +from django.core.urlresolvers import resolve +from reports.views import ReportView +from subjects.models import Subject, Tag +from users.models import User +from topics.models import Topic, Resource +from chat.models import Conversation, TalkMessages +from datetime import datetime +from log.models import Log +from django.db.models import Q +from ..views import most_used_tags +from django.http import HttpResponse, JsonResponse + +class APIDashBoardTest(TestCase): + + def setUp(self): + self.c = Client() + self.student = User.objects.create(username = "student01", email= "student01@amadeus.br", password="amadeus") + if self.c.login(email="student01@amadeus.br", password="amadeus"): + print("student01 logged in") + + + def test_most_used_tags(self): + t = Tag(name="felipe") + t.save() + t1 = Tag(name="b2") + t1.save() + + s1 = Subject.objects.create(name="subject", visible= True, init_date= datetime.now(), end_date= datetime.now(), + subscribe_begin = datetime.now(), subscribe_end= datetime.now() ) + s1.tags.add(t) + s1.save() + s2 = Subject.objects.create(name="subject dois", visible= True, init_date= datetime.now(), end_date= datetime.now(), + subscribe_begin = datetime.now(), subscribe_end= datetime.now() ) + s2.tags.add(t) + s2.save() + r1 = Resource.objects.create(name="resource um") + r1.tags.add(t1) + r1.save() + + + expected_data = {'felipe': 2, 'b2': 1} + data = self.c.get('/analytics/most_used_tags/') + print(data) + self.assertEqual(data, expected_data) diff --git a/analytics/urls.py b/analytics/urls.py new file mode 100644 index 0000000..8923fac --- /dev/null +++ b/analytics/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url, include +from . import views + + +urlpatterns = [ + url(r'^view/general/$', views.GeneralView.as_view(), name='view_general'), + + #"api" callls + url(r'^most_used_tags/$', views.most_used_tags, name="most_used_tags"), +] \ No newline at end of file diff --git a/analytics/views.py b/analytics/views.py new file mode 100644 index 0000000..546093f --- /dev/null +++ b/analytics/views.py @@ -0,0 +1,92 @@ +from django.shortcuts import render + +# Create your views here. +from django.views import generic +from django.db.models import Count + +from subjects.models import Tag, Subject +from topics.models import Resource +from users.models import User +from django.http import HttpResponse, JsonResponse + + +class GeneralView(generic.TemplateView): + template_name = "analytics/general.html" + + def get_context_data(self, **kwargs): + context = {} + + return context + + + +def most_used_tags(request): + tags = Tag.objects.all() + + data = {} + #grab all references to that tag + for tag in tags: + subjects_count = Subject.objects.filter(tags = tag).count() + if subjects_count > 0: + data[tag.name] = {'name': tag.name} + data[tag.name]['count'] = subjects_count + + resources_count = Resource.objects.filter(tags = tag).count() + if resources_count > 0: + if data.get(tag.name): + data[tag.name]['count'] = data[tag.name]['count'] + resources_count + else: + data[tag.name] = {'name': tag.name} + data[tag.name]['count'] = resources_count + + data = sorted(data.values(), key = lambda x: x['count'], reverse=True ) + data = data[:30] #get top 30 tags + return JsonResponse(data, safe= False) + + +def heatmap(request): + return None + + + +""" +Subject view that returns a list of the most used subjects """ + + +def most_acessed_subjects(request): + data = {} #empty response + + data = Log.objects.filter(resource = 'subject') + subjects = {} + for datum in data: + if datum.context: + subject_id = datum.context['subject_id'] + if subject_id in subjects.keys(): + subjects[subject_id]['count'] = subjects[subject_id]['count'] + 1 + else: + subjects[subject_id] = {'name': datum.context['subject_name'], 'count':0 } + + + #order the values of the dictionary by the count in descendent order + subjects = sorted(subjects.values(), key = lambda x: x['count'], reverse=True ) + subjects = subjects[:30] + + return JsonResponse(subjects, safe=False) + + + +def most_accessed_categories(request): + return None + +def most_accessed_resource_kind(request): + return None + + +def get_users_log(request): + fifty_users = Log.objects.values('user_id').annotate(count = Count('user_id')).order_by('-count')[:50] + fifty_users = list(fifty_users) + for user in fifty_users: + user_object = User.objects.get(id=user['user_id']) + user['image'] = user_object.image_url + user['user'] = user_object.social_name + return JsonResponse(fifty_users, safe=False) \ No newline at end of file -- libgit2 0.21.2