taskwarrior-web 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,15 @@
1
+ ## v1.0.7 (11/17/12)
2
+
3
+ * Moving `task` version parsing to Verionomy library. Should be much more
4
+ flexible and stable.
5
+ * Removing tag input widget from new task form, and changed parsing to split
6
+ tags on spaces, commas, or any combination thereof. So something like
7
+ "these,will all, be , tags now" should result in tags of "these", "will",
8
+ "all", "be", "tags", and "now". Hopefully this is easier to use and less
9
+ confusing.
10
+ * Adding basic support for `task` < 1.9.2
11
+ * Moved to thin gem as a server for speed and stability.
12
+
1
13
  ## v1.0.6 (11/15/12)
2
14
 
3
15
  * Fixed escaping issue when viewing or adding projects with spaces.
data/README.md CHANGED
@@ -3,12 +3,17 @@
3
3
  A lightweight, Sinatra-based web interface for the
4
4
  wonderful [Taskwarrior](http://taskwarrior.org/) todo application.
5
5
 
6
- **Now compatible with ALL versions of Taskwarrior, including the new 2.0.0**
6
+ **Now compatible with all versions of Taskwarrior, including 2.x**
7
7
  **Also, now including a NEW theme based on Twitter Bootstrap. Mobile version
8
8
  forthcoming!**
9
9
 
10
10
  [![Build Status](https://secure.travis-ci.org/theunraveler/taskwarrior-web.png)](http://travis-ci.org/theunraveler/taskwarrior-web)
11
11
 
12
+ ## Requirements
13
+
14
+ * `ruby` >= 1.9 (support for `ruby` < 1.9 is very unlikely, but pull requests
15
+ are gladly accepted).
16
+
12
17
  ## Installation
13
18
 
14
19
  `gem install taskwarrior-web`
@@ -38,10 +43,17 @@ The current featureset includes:
38
43
  * Optional Basic HTTP Auth protection. To enable, set `task-web.user` and
39
44
  `task-web.passwd` in your `.taskrc` file.
40
45
 
41
- ## Known Issues
46
+ ## Reporting Bugs
47
+
48
+ To report a bug, use the [Github issue tracker][1]. Since `taskwarrior-web`
49
+ works with several different versions of `task`, using many different
50
+ configurations, please include the output from `task _version` and either the
51
+ output of `task show` or a copy of your `.taskrc` file when filing a bug. This helps me reproduce bugs easier.
52
+
53
+ Here is an example of a [good bug report][2].
42
54
 
43
- * taskwarrior-web requires Ruby >= 1.9. It will not work with 1.8 and lower.
44
- Support for 1.8 will happen at some point.
55
+ [1]: http://github.com/theunraveler/taskwarrior-web/issues
56
+ [2]: http://github.com/theunraveler/taskwarrior-web/issues/26
45
57
 
46
58
  ## Marginalia
47
59
 
@@ -4,7 +4,7 @@ module TaskwarriorWeb
4
4
  module CommandBuilder
5
5
  def self.included(class_name)
6
6
  class_name.class_eval do
7
- case TaskwarriorWeb::Config.task_major_version
7
+ case TaskwarriorWeb::Config.version.major
8
8
  when 2
9
9
  require 'taskwarrior-web/command_builders/v2'
10
10
  include TaskwarriorWeb::CommandBuilder::V2
@@ -5,7 +5,7 @@ module TaskwarriorWeb::CommandBuilder
5
5
 
6
6
  TASK_COMMANDS = {
7
7
  :add => 'add',
8
- :query => '_query',
8
+ :query => TaskwarriorWeb::Config.version > Versionomy.parse('1.9.2') ? '_query' : 'export',
9
9
  :count => 'count',
10
10
  :complete => ':id done',
11
11
  :projects => '_projects',
@@ -42,8 +42,7 @@ module TaskwarriorWeb::CommandBuilder
42
42
  string = ''
43
43
  string << %Q( #{@params.delete(:description).shellescape}) if @params.has_key?(:description)
44
44
 
45
- if @params.has_key?(:tags)
46
- tags = @params.delete(:tags)
45
+ if tags = @params.delete(:tags)
47
46
  tag_indicator = TaskwarriorWeb::Config.property('tag.indicator') || '+'
48
47
  tags.each { |tag| string << %Q( #{tag_indicator}#{tag.to_s.shellescape}) }
49
48
  end
@@ -1,14 +1,11 @@
1
1
  require 'parseconfig'
2
+ require 'versionomy'
2
3
 
3
4
  module TaskwarriorWeb
4
5
  class Config
5
6
 
6
- def self.task_version
7
- @task_version ||= `task _version`
8
- end
9
-
10
- def self.task_major_version
11
- self.task_version[0,1].to_i
7
+ def self.version
8
+ @version ||= Versionomy.parse(`task _version`.strip)
12
9
  end
13
10
 
14
11
  def self.file
@@ -0,0 +1,16 @@
1
+ require 'taskwarrior-web/config'
2
+ require 'versionomy'
3
+
4
+ module TaskwarriorWeb
5
+ module Parser
6
+ def self.parse(results)
7
+ if TaskwarriorWeb::Config.version > Versionomy.parse('1.9.2')
8
+ require 'taskwarrior-web/parser/json'
9
+ TaskwarriorWeb::Parser::Json.parse(results)
10
+ else
11
+ require 'taskwarrior-web/parser/csv'
12
+ TaskwarriorWeb::Parser::Csv.parse(results)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ require 'csv'
2
+
3
+ module TaskwarriorWeb::Parser::Csv
4
+ def self.parse(csv)
5
+ rows = []
6
+ CSV.parse(csv, :headers => true, :quote_char => "'", :header_converters => :symbol, :converters => :all) do |row|
7
+ rows << Hash[row.headers.zip(row.fields)]
8
+ end
9
+ rows
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ require 'json'
2
+
3
+ module TaskwarriorWeb::Parser::Json
4
+ def self.parse(json)
5
+ json.strip!
6
+ json = '[' + json + ']'
7
+ json == '[No matches.]' ? [] : ::JSON.parse(json)
8
+ end
9
+ end
@@ -28,34 +28,25 @@ var initAutocomplete = function() {
28
28
  $('#task-project').typeahead({
29
29
  source: function (query, process) {
30
30
  return $.get('/ajax/projects/', {query: query}, function (data) {
31
- data = $.parseJSON(data);
32
- process(data);
31
+ process($.parseJSON(data));
33
32
  });
34
33
  }
35
34
  });
36
-
37
- $('#task-tags').tokenInput('/ajax/tags', {
38
- theme: 'facebook',
39
- queryParam: 'query',
40
- noResultsText: 'No matching tags',
41
- allowFreeTagging: true
42
- });
43
35
  };
44
36
 
45
37
  var initTaskCompletion = function() {
46
38
  $('input.complete').click(function() {
47
39
  // Cache the checkbox in case we need to restore it.
48
- var container = $(this).parent();
49
- var checkbox = container.html();
50
- var row = $(this).closest('tr');
40
+ var container = $(this).parent(),
41
+ checkbox = container.html(),
42
+ row = $(this).closest('tr');
51
43
  container.html('<img src="/img/ajax-loader.gif" />');
52
44
  $.ajax({
53
45
  url: '/ajax/task-complete/' + $(this).data('task-id'),
54
46
  type: 'POST',
55
47
  success: function(data) {
56
48
  refreshPageContents();
57
- var message = (data === '') ? 'Task marked as completed.' : data;
58
- set_message(message, 'alert-success');
49
+ set_message(data === '' ? 'Task marked as completed.' : data, 'alert-success');
59
50
  row.fadeOut();
60
51
  },
61
52
  error: function(data) {
@@ -1,5 +1,6 @@
1
- require 'json'
2
1
  require 'taskwarrior-web/command'
2
+ require 'taskwarrior-web/parser'
3
+ require 'versionomy'
3
4
 
4
5
  module TaskwarriorWeb
5
6
 
@@ -32,7 +33,7 @@ module TaskwarriorWeb
32
33
 
33
34
  # Make sure that the tags are an array.
34
35
  def tags=(value)
35
- @tags = value.is_a?(String) ? value.gsub(', ', ',').split(',') : value
36
+ @tags = value.is_a?(String) ? value.split(/\W+/).reject(&:empty?) : value
36
37
  end
37
38
 
38
39
  def to_hash
@@ -46,20 +47,18 @@ module TaskwarriorWeb
46
47
  # Run queries on tasks.
47
48
  def self.query(*args)
48
49
  tasks = []
49
-
50
- # Process the JSON data.
51
- json = Command.new(:query, nil, *args).run
52
- json.strip!
53
- json = '[' + json + ']'
54
- results = json == '[No matches.]' ? [] : ::JSON.parse(json)
55
-
56
- results.each { |result| tasks << Task.new(result) }
50
+ results = Command.new(:query, nil, *args).run
51
+ Parser.parse(results).each { |result| tasks << Task.new(result) }
57
52
  tasks
58
53
  end
59
54
 
60
55
  # Get the number of tasks for some paramters
61
56
  def self.count(*args)
62
- Command.new(:count, nil, *args).run.to_s.strip!
57
+ if TaskwarriorWeb::Config.version > Versionomy.parse('1.9.2')
58
+ Command.new(:count, nil, *args).run.to_s.strip!
59
+ else
60
+ self.query(*args).count
61
+ end
63
62
  end
64
63
 
65
64
  # Define method_missing to implement dynamic finder methods
@@ -1,19 +1,16 @@
1
1
  <html>
2
2
  <head>
3
- <title><%= @title %> | Taskwarrior</title>
3
+ <title><%= @title %> | TaskwarriorWeb</title>
4
4
 
5
5
  <!-- Styles -->
6
6
  <link rel="stylesheet" href="/css/bootstrap.min.css">
7
7
  <link rel="stylesheet" href="/css/datepicker.css">
8
- <link rel="stylesheet" href="/css/token-input.css">
9
- <link rel="stylesheet" href="/css/token-input-facebook.css">
10
8
  <link rel="stylesheet" href="/css/styles.css">
11
9
 
12
10
  <!-- Scripts -->
13
11
  <script type="text/javascript" src="/js/jquery.min.js"></script>
14
12
  <script type="text/javascript" src="/js/bootstrap.min.js"></script>
15
13
  <script type="text/javascript" src="/js/bootstrap-datepicker.js"></script>
16
- <script type="text/javascript" src="/js/jquery.tokeninput.js"></script>
17
14
  <script type="text/javascript" src="/js/application.js"></script>
18
15
  </head>
19
16
 
@@ -10,7 +10,7 @@
10
10
  <div class="control-group">
11
11
  <label for="task-project" class="control-label">Project</label>
12
12
  <div class="controls">
13
- <input type="textfield" id="task-project" name="task[project]" value="<%= @task[:project] unless @task.nil? %>" />
13
+ <input type="textfield" id="task-project" name="task[project]" value="<%= @task[:project] unless @task.nil? %>" autocomplete="off" />
14
14
  </div>
15
15
  </div>
16
16
 
@@ -24,8 +24,8 @@
24
24
  <div class="control-group">
25
25
  <label for="task-tags" class="control-label">Tags</label>
26
26
  <div class="controls">
27
- <input type="textfield" id="task-tags" name="task[tags]" value="<%= @task[:tags] unless @task.nil? %>" />
28
- <div class="description">Enter tags separated by commas</div>
27
+ <input type="textfield" id="task-tags" name="task[tags]" value="<%= @task[:tags] unless @task.nil? %>" autocomplete="off" />
28
+ <div class="description">Enter tags separated by commas or spaces (e.g. <em>each, word will,be a tag</em>)</div>
29
29
  </div>
30
30
  </div>
31
31
 
@@ -44,4 +44,16 @@ describe "My App" do
44
44
  last_response.body.should eq('15')
45
45
  end
46
46
  end
47
+
48
+ describe 'not_found' do
49
+ it 'should set the title to "Not Found"' do
50
+ get '/page-not-found'
51
+ last_response.body.should =~ /<title>Page Not Found/
52
+ end
53
+
54
+ it 'should have a status code of 404' do
55
+ get '/page-not-found'
56
+ last_response.status.should eq(404)
57
+ end
58
+ end
47
59
  end
@@ -7,7 +7,7 @@ describe TaskwarriorWeb::CommandBuilder do
7
7
  describe '.included' do
8
8
  context 'when v2 is reported' do
9
9
  it 'should include CommandBuilder V2 module' do
10
- TaskwarriorWeb::Config.should_receive(:task_version).and_return('2.0.1')
10
+ TaskwarriorWeb::Config.should_receive(:version).and_return(Versionomy.parse('2.0.0'))
11
11
  TestCommandClass.class_eval { include TaskwarriorWeb::CommandBuilder }
12
12
  TestCommandClass.should include(TaskwarriorWeb::CommandBuilder::V2)
13
13
  end
@@ -15,7 +15,7 @@ describe TaskwarriorWeb::CommandBuilder do
15
15
 
16
16
  context 'when v1 is reported' do
17
17
  it 'should include CommandBuilder V1 module' do
18
- TaskwarriorWeb::Config.should_receive(:task_version).and_return('1.9.4')
18
+ TaskwarriorWeb::Config.should_receive(:version).and_return(Versionomy.parse('1.0.0'))
19
19
  TestCommandClass.class_eval { include TaskwarriorWeb::CommandBuilder }
20
20
  TestCommandClass.should include(TaskwarriorWeb::CommandBuilder::V1)
21
21
  end
@@ -23,7 +23,7 @@ describe TaskwarriorWeb::CommandBuilder do
23
23
 
24
24
  context 'when an invalid version number is reported' do
25
25
  it 'should throw an UnrecognizedTaskVersion exception' do
26
- TaskwarriorWeb::Config.should_receive(:task_version).and_return('95.583.3')
26
+ TaskwarriorWeb::Config.should_receive(:version).and_return(Versionomy.parse('9.0.0'))
27
27
  expect {
28
28
  TestCommandClass.class_eval { include TaskwarriorWeb::CommandBuilder }
29
29
  }.to raise_exception(TaskwarriorWeb::UnrecognizedTaskVersion)
@@ -17,7 +17,7 @@ describe TaskwarriorWeb::CommandBuilder::Base do
17
17
  it 'should create a string from the passed paramters' do
18
18
  command = TaskwarriorWeb::Command.new(:query, nil, :test => 14, :none => :none, :hello => :hi)
19
19
  command.parse_params
20
- command.params.should eq(' test:14 none:none hello:hi')
20
+ command.params.should eq(' test:\"14\" none:\"none\" hello:\"hi\"')
21
21
  end
22
22
 
23
23
  it 'should prefix tags with the tag.indicator if specified' do
@@ -37,7 +37,7 @@ describe TaskwarriorWeb::CommandBuilder::Base do
37
37
  it 'should pull out the description parameter' do
38
38
  command = TaskwarriorWeb::Command.new(:add, nil, :description => 'Hello', :status => :pending)
39
39
  command.parse_params
40
- command.params.should eq(" Hello status:pending")
40
+ command.params.should eq(' Hello status:\"pending\"')
41
41
  end
42
42
  end
43
43
  end
@@ -53,8 +53,8 @@ describe TaskwarriorWeb::Task do
53
53
 
54
54
  describe '#tags=' do
55
55
  it 'should convert a string to an array when initializing' do
56
- task = TaskwarriorWeb::Task.new(:tags => 'hi there, twice')
57
- task.tags.should eq(['hi there', 'twice'])
56
+ task = TaskwarriorWeb::Task.new(:tags => 'hi, twice')
57
+ task.tags.should eq(['hi', 'twice'])
58
58
  end
59
59
 
60
60
  it 'should convert a string to an array when setting explicitly' do
@@ -62,6 +62,15 @@ describe TaskwarriorWeb::Task do
62
62
  task.tags = 'hello, twice,thrice'
63
63
  task.tags.should eq(['hello', 'twice', 'thrice'])
64
64
  end
65
+
66
+ it 'should break on any combination of spaces and commas' do
67
+ try = ['hi twice and again', 'hi,twice,and,again', 'hi twice,and again', 'hi, twice and , again']
68
+
69
+ try.each do |tags|
70
+ task = TaskwarriorWeb::Task.new(:tags => tags)
71
+ task.tags.should eq(['hi', 'twice', 'and', 'again'])
72
+ end
73
+ end
65
74
  end
66
75
 
67
76
  describe '#to_hash' do
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "taskwarrior-web"
6
- s.version = '1.0.6'
6
+ s.version = '1.0.7'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Jake Bell"]
9
9
  s.email = ["jake@theunraveler.com"]
@@ -16,9 +16,11 @@ Gem::Specification.new do |s|
16
16
  s.required_ruby_version = '>= 1.9.0'
17
17
 
18
18
  s.add_dependency('sinatra')
19
+ s.add_dependency('thin')
19
20
  s.add_dependency('parseconfig')
20
21
  s.add_dependency('vegas')
21
22
  s.add_dependency('rinku')
23
+ s.add_dependency('versionomy')
22
24
 
23
25
  s.add_development_dependency('rake')
24
26
  s.add_development_dependency('rack-test')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taskwarrior-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-14 00:00:00.000000000 Z
12
+ date: 2012-11-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
16
- requirement: &70175087714260 !ruby/object:Gem::Requirement
16
+ requirement: &70252814870220 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,21 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70175087714260
24
+ version_requirements: *70252814870220
25
+ - !ruby/object:Gem::Dependency
26
+ name: thin
27
+ requirement: &70252814869540 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70252814869540
25
36
  - !ruby/object:Gem::Dependency
26
37
  name: parseconfig
27
- requirement: &70175087713800 !ruby/object:Gem::Requirement
38
+ requirement: &70252814868880 !ruby/object:Gem::Requirement
28
39
  none: false
29
40
  requirements:
30
41
  - - ! '>='
@@ -32,10 +43,10 @@ dependencies:
32
43
  version: '0'
33
44
  type: :runtime
34
45
  prerelease: false
35
- version_requirements: *70175087713800
46
+ version_requirements: *70252814868880
36
47
  - !ruby/object:Gem::Dependency
37
48
  name: vegas
38
- requirement: &70175087713200 !ruby/object:Gem::Requirement
49
+ requirement: &70252814868200 !ruby/object:Gem::Requirement
39
50
  none: false
40
51
  requirements:
41
52
  - - ! '>='
@@ -43,10 +54,21 @@ dependencies:
43
54
  version: '0'
44
55
  type: :runtime
45
56
  prerelease: false
46
- version_requirements: *70175087713200
57
+ version_requirements: *70252814868200
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: rinku
49
- requirement: &70175087712780 !ruby/object:Gem::Requirement
60
+ requirement: &70252814867560 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70252814867560
69
+ - !ruby/object:Gem::Dependency
70
+ name: versionomy
71
+ requirement: &70252814866800 !ruby/object:Gem::Requirement
50
72
  none: false
51
73
  requirements:
52
74
  - - ! '>='
@@ -54,10 +76,10 @@ dependencies:
54
76
  version: '0'
55
77
  type: :runtime
56
78
  prerelease: false
57
- version_requirements: *70175087712780
79
+ version_requirements: *70252814866800
58
80
  - !ruby/object:Gem::Dependency
59
81
  name: rake
60
- requirement: &70175087712300 !ruby/object:Gem::Requirement
82
+ requirement: &70252814866040 !ruby/object:Gem::Requirement
61
83
  none: false
62
84
  requirements:
63
85
  - - ! '>='
@@ -65,10 +87,10 @@ dependencies:
65
87
  version: '0'
66
88
  type: :development
67
89
  prerelease: false
68
- version_requirements: *70175087712300
90
+ version_requirements: *70252814866040
69
91
  - !ruby/object:Gem::Dependency
70
92
  name: rack-test
71
- requirement: &70175087711700 !ruby/object:Gem::Requirement
93
+ requirement: &70252814865320 !ruby/object:Gem::Requirement
72
94
  none: false
73
95
  requirements:
74
96
  - - ! '>='
@@ -76,10 +98,10 @@ dependencies:
76
98
  version: '0'
77
99
  type: :development
78
100
  prerelease: false
79
- version_requirements: *70175087711700
101
+ version_requirements: *70252814865320
80
102
  - !ruby/object:Gem::Dependency
81
103
  name: rspec
82
- requirement: &70175087711220 !ruby/object:Gem::Requirement
104
+ requirement: &70252814864780 !ruby/object:Gem::Requirement
83
105
  none: false
84
106
  requirements:
85
107
  - - ! '>='
@@ -87,7 +109,7 @@ dependencies:
87
109
  version: '0'
88
110
  type: :development
89
111
  prerelease: false
90
- version_requirements: *70175087711220
112
+ version_requirements: *70252814864780
91
113
  description: This gem provides a graphical frontend for the Taskwarrior task manager.
92
114
  It is based on Sinatra.
93
115
  email:
@@ -116,13 +138,13 @@ files:
116
138
  - lib/taskwarrior-web/command_builders/v2.rb
117
139
  - lib/taskwarrior-web/config.rb
118
140
  - lib/taskwarrior-web/helpers.rb
141
+ - lib/taskwarrior-web/parser.rb
142
+ - lib/taskwarrior-web/parser/csv.rb
143
+ - lib/taskwarrior-web/parser/json.rb
119
144
  - lib/taskwarrior-web/public/css/bootstrap-responsive.min.css
120
145
  - lib/taskwarrior-web/public/css/bootstrap.min.css
121
146
  - lib/taskwarrior-web/public/css/datepicker.css
122
- - lib/taskwarrior-web/public/css/jquery.tagsinput.css
123
147
  - lib/taskwarrior-web/public/css/styles.css
124
- - lib/taskwarrior-web/public/css/token-input-facebook.css
125
- - lib/taskwarrior-web/public/css/token-input.css
126
148
  - lib/taskwarrior-web/public/favicon.ico
127
149
  - lib/taskwarrior-web/public/img/ajax-loader.gif
128
150
  - lib/taskwarrior-web/public/img/glyphicons-halflings-white.png
@@ -131,7 +153,6 @@ files:
131
153
  - lib/taskwarrior-web/public/js/bootstrap-datepicker.js
132
154
  - lib/taskwarrior-web/public/js/bootstrap.min.js
133
155
  - lib/taskwarrior-web/public/js/jquery.min.js
134
- - lib/taskwarrior-web/public/js/jquery.tokeninput.js
135
156
  - lib/taskwarrior-web/runner.rb
136
157
  - lib/taskwarrior-web/task.rb
137
158
  - lib/taskwarrior-web/views/404.erb
@@ -175,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
196
  version: '0'
176
197
  segments:
177
198
  - 0
178
- hash: -3999339187514199251
199
+ hash: -2453576857035471181
179
200
  requirements: []
180
201
  rubyforge_project: taskwarrior-web
181
202
  rubygems_version: 1.8.11