diff --git a/app/assets/javascripts/admin/models/report.js b/app/assets/javascripts/admin/models/report.js
new file mode 100644
index 00000000000..5acca7dcec5
--- /dev/null
+++ b/app/assets/javascripts/admin/models/report.js
@@ -0,0 +1,15 @@
+Discourse.Report = Discourse.Model.extend({});
+
+Discourse.Report.reopenClass({
+ find: function(type) {
+ var model = Discourse.Report.create();
+ jQuery.ajax("/admin/reports/" + type, {
+ type: 'GET',
+ success: function(json) {
+ model.mergeAttributes(json.report);
+ model.set('loaded', true);
+ }
+ });
+ return(model);
+ }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/routes/admin_reports_route.js b/app/assets/javascripts/admin/routes/admin_reports_route.js
new file mode 100644
index 00000000000..46a97b31632
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin_reports_route.js
@@ -0,0 +1,9 @@
+Discourse.AdminReportsRoute = Discourse.Route.extend({
+ model: function(params) {
+ return(Discourse.Report.find(params.type));
+ },
+
+ renderTemplate: function() {
+ this.render('admin/templates/reports', {into: 'admin/templates/admin'});
+ }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/routes/admin_routes.js b/app/assets/javascripts/admin/routes/admin_routes.js
index f3a2a09a47b..7e6c0f4f746 100644
--- a/app/assets/javascripts/admin/routes/admin_routes.js
+++ b/app/assets/javascripts/admin/routes/admin_routes.js
@@ -11,6 +11,8 @@ Discourse.Route.buildRoutes(function() {
this.route('email_logs', { path: '/email_logs' });
this.route('customize', { path: '/customize' });
+ this.resource('adminReports', { path: '/reports/:type' });
+
this.resource('adminFlags', { path: '/flags' }, function() {
this.route('active', { path: '/active' });
this.route('old', { path: '/old' });
diff --git a/app/assets/javascripts/admin/templates/reports.js.handlebars b/app/assets/javascripts/admin/templates/reports.js.handlebars
new file mode 100644
index 00000000000..6139ebbb637
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/reports.js.handlebars
@@ -0,0 +1,20 @@
+{{#if content.loaded}}
+
{{content.title}}
+
+
+
+ {{content.xaxis}} |
+ {{content.yaxis}} |
+
+
+ {{#each content.data}}
+
+ {{x}} |
+ {{y}} |
+
+ {{/each}}
+
+
+{{else}}
+ {{i18n loading}}
+{{/if}}
\ No newline at end of file
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
new file mode 100644
index 00000000000..8b2d4f066fc
--- /dev/null
+++ b/app/controllers/admin/reports_controller.rb
@@ -0,0 +1,17 @@
+require_dependency 'report'
+
+class Admin::ReportsController < Admin::AdminController
+
+ def show
+
+ report_type = params[:type]
+
+ raise Discourse::NotFound.new unless report_type =~ /^[a-z0-9\_]+$/
+
+ report = Report.find(report_type)
+ raise Discourse::NotFound.new if report.blank?
+
+ render_json_dump(report: report)
+ end
+
+end
diff --git a/app/models/report.rb b/app/models/report.rb
new file mode 100644
index 00000000000..82e6ef2ba44
--- /dev/null
+++ b/app/models/report.rb
@@ -0,0 +1,37 @@
+class Report
+
+ attr_accessor :type, :data
+
+ def initialize(type)
+ @type = type
+ @data = nil
+ end
+
+ def as_json
+ {
+ type: self.type,
+ title: I18n.t("reports.#{self.type}.title"),
+ xaxis: I18n.t("reports.#{self.type}.xaxis"),
+ yaxis: I18n.t("reports.#{self.type}.yaxis"),
+ data: self.data
+ }
+ end
+
+ def self.find(type)
+ report_method = :"report_#{type}"
+ return nil unless respond_to?(report_method)
+
+ # Load the report
+ report = Report.new(type)
+ send(report_method, report)
+ report
+ end
+
+ def self.report_visits(report)
+ report.data = []
+ UserVisit.by_day.each do |date, count|
+ report.data << {x: date, y: count}
+ end
+ end
+
+end
diff --git a/app/models/user_visit.rb b/app/models/user_visit.rb
index 17a937ebb5c..1510f24e788 100644
--- a/app/models/user_visit.rb
+++ b/app/models/user_visit.rb
@@ -1,3 +1,9 @@
class UserVisit < ActiveRecord::Base
attr_accessible :visited_at, :user_id
+
+ # A list of visits in the last month by day
+ def self.by_day
+ where("visited_at > ?", 1.month.ago).group(:visited_at).order(:visited_at).count
+ end
+
end
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 02e1133080b..0aa3963cd03 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -230,6 +230,12 @@ en:
title: "Re-Subscribed!"
description: "You have been re-subscribed."
+ reports:
+ visits:
+ title: "Users Visits by Day"
+ xaxis: "Day"
+ yaxis: "Visits"
+
site_settings:
min_post_length: "Minimum post length in characters"
max_post_length: "Maximum post length in characters"
diff --git a/config/routes.rb b/config/routes.rb
index b153d30c245..c78878ed12a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -24,6 +24,8 @@ Discourse::Application.routes.draw do
get '' => 'admin#index'
resources :site_settings
+ get 'reports/:type' => 'reports#show'
+
resources :users, id: USERNAME_ROUTE_FORMAT do
collection do
get 'list/:query' => 'users#index'
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
new file mode 100644
index 00000000000..40c2b1764c5
--- /dev/null
+++ b/spec/controllers/admin/reports_controller_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Admin::ReportsController do
+
+ it "is a subclass of AdminController" do
+ (Admin::ReportsController < Admin::AdminController).should be_true
+ end
+
+ context 'while logged in as an admin' do
+ let!(:admin) { log_in(:admin) }
+ let(:user) { Fabricate(:user) }
+
+
+ context '.show' do
+
+ context "invalid id form" do
+ let(:invalid_id) { "!!&asdfasdf" }
+
+ it "never calls Report.find" do
+ Report.expects(:find).never
+ xhr :get, :show, type: invalid_id
+ end
+
+ it "returns 404" do
+ xhr :get, :show, type: invalid_id
+ response.status.should == 404
+ end
+ end
+
+ context "valid type form" do
+
+ context 'missing report' do
+ before do
+ Report.expects(:find).with('active').returns(nil)
+ xhr :get, :show, type: 'active'
+ end
+
+ it "renders the report as JSON" do
+ response.status.should == 404
+ end
+ end
+
+ context 'a report is found' do
+ before do
+ Report.expects(:find).with('active').returns(Report.new('active'))
+ xhr :get, :show, type: 'active'
+ end
+
+ it "renders the report as JSON" do
+ response.should be_success
+ end
+
+ it "renders the report as JSON" do
+ ::JSON.parse(response.body).should be_present
+ end
+
+ end
+
+ end
+
+ end
+
+ end
+
+end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
new file mode 100644
index 00000000000..4c9c0c5d997
--- /dev/null
+++ b/spec/models/report_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Report do
+
+
+ describe 'visits report' do
+
+ let(:report) { Report.find('visits') }
+
+ context "no visits" do
+ it "returns an empty report" do
+ report.data.should be_blank
+ end
+ end
+
+ context "with visits" do
+ let(:user) { Fabricate(:user) }
+
+ before do
+ user.user_visits.create(visited_at: 1.day.ago)
+ user.user_visits.create(visited_at: 2.days.ago)
+ end
+
+ it "returns a report with data" do
+ report.data.should be_present
+ end
+
+ end
+
+
+ end
+
+
+end