diff --git a/.gitignore b/.gitignore index 4f06bb9b..bb909eb6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ tmp/ .DS_Store *.swp .redcar +bin/ +coverage +config/config.yml diff --git a/.rvmrc b/.rvmrc deleted file mode 100644 index 7e673dc9..00000000 --- a/.rvmrc +++ /dev/null @@ -1 +0,0 @@ -rvm use --create ruby-1.9.2-p290@hackety-hack.com diff --git a/.travis.yml b/.travis.yml index 69338f37..a9a71640 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,12 @@ -script: "rake spec && rake cucumber" -rvm: 1.9.2 +language: ruby +script: "bundle exec rake spec cucumber" +rvm: 1.9.3 +services: mongodb notifications: + email: + - tobias.pfeiffer@student.hpi.uni-potsdam.de + - james+travis-ci-broken@jamesrgifford.com + - kh@kalonhinds.com + - steven+travis-ci@nuclearsandwich.com irc: "irc.freenode.org#hacketyhack" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..049e26d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Thanks for helping! # + +## Questions ## + +If you're asking a question about Hackety Hack itself, check out the [Hackety +Hack repository][hh]. +Otherwise, go ahead and open an [issue][issues] and let us know! + +## Bugs ## + +If you notice a bug in Hackety-Hack.com, this is the place to let us know. +Please tell us: + +- Which url the bug occurred at +- What steps we can take to reproduce this bug +- If the bug is visual, including a screenshot is really helpful. + +## Pull Requests ## + +We :heart: pull requests; We :heart::blue_heart::green_heart: Pull Requests with tests. In fact, we don't want to accept pull requests without relevant tests. If you're not sure if the feature you want is welcome and you want to check with us, feel free to create [an issue][issues] or if you're just totally driven to make it happen, spike it out and open a pull request, but we'll ask you to add tests before it's merged. + + +[hh]: https://github.com/hacketyhack/hacketyhack +[issues]: https://github.com/hacketyhack/hackety-hack.com/issues diff --git a/Gemfile b/Gemfile index 7cdb7648..0e9edb9f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,44 +1,52 @@ source 'http://rubygems.org' -gem 'rails', '3.1.0' +#ruby=1.9.3-p448 +#ruby-gemset=hackety-hack.com + +ruby '1.9.3' + +gem 'rails', '3.2.17' gem 'json' +gem 'hackety_hack-lessons', '~> 1.1', :require => 'hackety_hack/lessons' + gem 'haml-rails' gem 'jquery-rails' gem 'mongo_mapper' gem 'bson_ext' -# Gems used only for assets and not required -# in production environments by default. -group :assets do - gem 'sass-rails', " ~> 3.1.0" - gem 'coffee-rails', "~> 3.1.0" - gem 'uglifier' -end - -gem 'jnunemaker-validatable', '>= 1.8.4' # Required by mm-devise -gem 'devise', '>= 1.2' -gem 'mm-devise', '>= 1.2' +gem "letter_opener", group: :development + +gem "devise", "~> 2.2.2" +gem 'mm-devise', '~> 2.0' gem 'cancan' gem 'inherited_resources' -gem 'simple_form', :git => "https://github.com/bitzesty/bootstrap_form.git" -gem "semantic_menu", :git => "git://github.com/michaek/semantic_menu.git" +gem 'simple_form', git: "https://github.com/bitzesty/bootstrap_form.git" +gem "semantic_menu", git: "git://github.com/michaek/semantic_menu.git" gem 'will_paginate' # Pagination -gem 'rdiscount' # Markdown +gem 'redcarpet', '~> 3.0' # Markdown +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails' + gem 'coffee-rails' + gem 'uglifier' +end group :development do # Use unicorn as the web server gem 'unicorn' end - group :development, :test do + gem 'debugger' gem 'fabrication' gem 'rspec-rails' - gem 'cucumber-rails' + gem 'capybara' + gem 'cucumber-rails', require: false gem "faker" gem 'pry' gem 'sqlite3' @@ -49,6 +57,9 @@ group :production do end group :test do + gem "simplecov", :require => false + gem "coveralls" gem "mocha" gem "database_cleaner" + gem "launchy" end diff --git a/Gemfile.lock b/Gemfile.lock index 94dbbe34..98ac5970 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,217 +15,234 @@ GIT GEM remote: http://rubygems.org/ specs: - actionmailer (3.1.0) - actionpack (= 3.1.0) - mail (~> 2.3.0) - actionpack (3.1.0) - activemodel (= 3.1.0) - activesupport (= 3.1.0) + actionmailer (3.2.17) + actionpack (= 3.2.17) + mail (~> 2.5.4) + actionpack (3.2.17) + activemodel (= 3.2.17) + activesupport (= 3.2.17) builder (~> 3.0.0) erubis (~> 2.7.0) - i18n (~> 0.6) - rack (~> 1.3.2) - rack-cache (~> 1.0.3) - rack-mount (~> 0.8.2) + journey (~> 1.0.4) + rack (~> 1.4.5) + rack-cache (~> 1.2) rack-test (~> 0.6.1) - sprockets (~> 2.0.0) - activemodel (3.1.0) - activesupport (= 3.1.0) - bcrypt-ruby (~> 3.0.0) + sprockets (~> 2.2.1) + activemodel (3.2.17) + activesupport (= 3.2.17) builder (~> 3.0.0) - i18n (~> 0.6) - activerecord (3.1.0) - activemodel (= 3.1.0) - activesupport (= 3.1.0) - arel (~> 2.2.1) + activerecord (3.2.17) + activemodel (= 3.2.17) + activesupport (= 3.2.17) + arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.1.0) - activemodel (= 3.1.0) - activesupport (= 3.1.0) - activesupport (3.1.0) + activeresource (3.2.17) + activemodel (= 3.2.17) + activesupport (= 3.2.17) + activesupport (3.2.17) + i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) - arel (2.2.1) - bcrypt-ruby (3.0.1) - bson (1.3.1) - bson_ext (1.3.1) - builder (3.0.0) - cancan (1.6.7) - capybara (1.1.1) + addressable (2.3.5) + arel (3.0.3) + bcrypt-ruby (3.1.2) + bson (1.9.2) + bson_ext (1.9.2) + bson (~> 1.9.2) + builder (3.0.4) + cancan (1.6.10) + capybara (2.2.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - selenium-webdriver (~> 2.0) - xpath (~> 0.1.4) - childprocess (0.2.2) - ffi (~> 1.0.6) - coderay (0.9.8) - coffee-rails (3.1.1) + xpath (~> 2.0) + coderay (1.1.0) + coffee-rails (3.2.2) coffee-script (>= 2.2.0) - railties (~> 3.1.0) + railties (~> 3.2.0) coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.1.2) - cucumber (1.1.1) + coffee-script-source (1.7.0) + columnize (0.9.0) + coveralls (0.7.0) + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + term-ansicolor + thor + cucumber (1.3.10) builder (>= 2.1.2) - diff-lcs (>= 1.1.2) - gherkin (~> 2.6.0) - json (>= 1.4.6) - term-ansicolor (>= 1.0.6) - cucumber-rails (1.1.1) - capybara (>= 1.1.1) - cucumber (>= 1.1.0) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.0.2) + cucumber-rails (1.4.0) + capybara (>= 1.1.2) + cucumber (>= 1.2.0) nokogiri (>= 1.5.0) - database_cleaner (0.6.7) - devise (1.4.9) + rails (>= 3.0.0) + database_cleaner (1.2.0) + debugger (1.6.8) + columnize (>= 0.3.1) + debugger-linecache (~> 1.2.0) + debugger-ruby_core_source (~> 1.3.5) + debugger-linecache (1.2.0) + debugger-ruby_core_source (1.3.8) + devise (2.2.8) bcrypt-ruby (~> 3.0) - orm_adapter (~> 0.0.3) - warden (~> 1.0.3) - diff-lcs (1.1.3) + orm_adapter (~> 0.1) + railties (~> 3.1) + warden (~> 1.2.1) + diff-lcs (1.2.5) + docile (1.1.3) erubis (2.7.0) - execjs (1.2.9) - multi_json (~> 1.0) - fabrication (1.2.0) - faker (1.0.1) - i18n (~> 0.4) - ffi (1.0.9) - gherkin (2.6.1) - json (>= 1.4.6) - haml (3.1.3) - haml-rails (0.3.4) - actionpack (~> 3.0) - activesupport (~> 3.0) - haml (~> 3.0) - railties (~> 3.0) - has_scope (0.5.1) - hike (1.2.1) - i18n (0.6.0) - inherited_resources (1.3.0) - has_scope (~> 0.5.0) - responders (~> 0.6.0) - jnunemaker-validatable (1.8.4) - activesupport (>= 2.3.4) - jquery-rails (1.0.16) - railties (~> 3.0) - thor (~> 0.14) - json (1.6.1) - json_pure (1.6.1) - kgio (2.6.0) - mail (2.3.0) - i18n (>= 0.4.0) + execjs (2.0.2) + fabrication (2.9.8) + faker (1.2.0) + i18n (~> 0.5) + gherkin (2.12.2) + multi_json (~> 1.3) + hackety_hack-lessons (1.1.2) + metadown + haml (4.0.5) + tilt + haml-rails (0.4) + actionpack (>= 3.1, < 4.1) + activesupport (>= 3.1, < 4.1) + haml (>= 3.1, < 4.1) + railties (>= 3.1, < 4.1) + has_scope (0.6.0.rc) + actionpack (>= 3.2, < 5) + activesupport (>= 3.2, < 5) + hike (1.2.3) + i18n (0.6.9) + inherited_resources (1.4.1) + has_scope (~> 0.6.0.rc) + responders (~> 1.0.0.rc) + journey (1.0.4) + jquery-rails (3.1.0) + railties (>= 3.0, < 5.0) + thor (>= 0.14, < 2.0) + json (1.8.1) + kgio (2.9.2) + launchy (2.4.2) + addressable (~> 2.3) + letter_opener (1.2.0) + launchy (~> 2.2) + mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - metaclass (0.0.1) - method_source (0.6.7) - ruby_parser (>= 2.3.1) - mime-types (1.17.2) - mm-devise (1.3) + metaclass (0.0.3) + metadown (1.0.1) + redcarpet + method_source (0.8.2) + mime-types (1.25.1) + mini_portile (0.5.2) + mm-devise (2.0) devise (>= 1.2) mongo_mapper (>= 0.9.0) - mocha (0.10.0) + mocha (1.0.0) metaclass (~> 0.0.1) - mongo (1.3.1) - bson (>= 1.3.1) - mongo_mapper (0.9.2) + mongo (1.9.2) + bson (~> 1.9.2) + mongo_mapper (0.12.0) activemodel (~> 3.0) activesupport (~> 3.0) - plucky (~> 0.3.8) - multi_json (1.0.3) - nokogiri (1.5.0) - orm_adapter (0.0.5) - pg (0.11.0) - plucky (0.3.8) - mongo (~> 1.3) - polyglot (0.3.2) - pry (0.9.7.3) - coderay (~> 0.9.8) - method_source (~> 0.6.7) - ruby_parser (>= 2.3.1) - slop (~> 2.1.0) - rack (1.3.5) - rack-cache (1.0.3) + plucky (~> 0.5.2) + multi_json (1.8.4) + multi_test (0.0.3) + nokogiri (1.6.1) + mini_portile (~> 0.5.0) + orm_adapter (0.5.0) + pg (0.17.1) + plucky (0.5.2) + mongo (~> 1.5) + polyglot (0.3.4) + pry (0.9.12.6) + coderay (~> 1.0) + method_source (~> 0.8) + slop (~> 3.4) + rack (1.4.5) + rack-cache (1.2) rack (>= 0.4) - rack-mount (0.8.3) - rack (>= 1.0.0) - rack-ssl (1.3.2) + rack-ssl (1.3.3) rack - rack-test (0.6.1) + rack-test (0.6.2) rack (>= 1.0) - rails (3.1.0) - actionmailer (= 3.1.0) - actionpack (= 3.1.0) - activerecord (= 3.1.0) - activeresource (= 3.1.0) - activesupport (= 3.1.0) + rails (3.2.17) + actionmailer (= 3.2.17) + actionpack (= 3.2.17) + activerecord (= 3.2.17) + activeresource (= 3.2.17) + activesupport (= 3.2.17) bundler (~> 1.0) - railties (= 3.1.0) - railties (3.1.0) - actionpack (= 3.1.0) - activesupport (= 3.1.0) + railties (= 3.2.17) + railties (3.2.17) + actionpack (= 3.2.17) + activesupport (= 3.2.17) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) - thor (~> 0.14.6) - raindrops (0.8.0) - rake (0.9.2.2) - rdiscount (1.6.8) - rdoc (3.11) + thor (>= 0.14.6, < 2.0) + raindrops (0.13.0) + rake (10.1.1) + rdoc (3.12.2) json (~> 1.4) - responders (0.6.4) - rspec (2.7.0) - rspec-core (~> 2.7.0) - rspec-expectations (~> 2.7.0) - rspec-mocks (~> 2.7.0) - rspec-core (2.7.1) - rspec-expectations (2.7.0) - diff-lcs (~> 1.1.2) - rspec-mocks (2.7.0) - rspec-rails (2.7.0) - actionpack (~> 3.0) - activesupport (~> 3.0) - railties (~> 3.0) - rspec (~> 2.7.0) - ruby_parser (2.3.1) - sexp_processor (~> 3.0) - rubyzip (0.9.4) - sass (3.1.10) - sass-rails (3.1.4) - actionpack (~> 3.1.0) - railties (~> 3.1.0) - sass (>= 3.1.4) - sprockets (~> 2.0.0) - tilt (~> 1.3.2) - selenium-webdriver (2.10.0) - childprocess (>= 0.2.1) - ffi (= 1.0.9) - json_pure - rubyzip - sexp_processor (3.0.7) - slop (2.1.0) - sprockets (2.0.3) + redcarpet (3.1.1) + responders (1.0.0) + railties (>= 3.2, < 5) + rest-client (1.6.7) + mime-types (>= 1.16) + rspec-core (2.14.7) + rspec-expectations (2.14.5) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.5) + rspec-rails (2.14.1) + actionpack (>= 3.0) + activemodel (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + sass (3.2.14) + sass-rails (3.2.6) + railties (~> 3.2.0) + sass (>= 3.1.10) + tilt (~> 1.3) + simplecov (0.8.2) + docile (~> 1.1.0) + multi_json + simplecov-html (~> 0.8.0) + simplecov-html (0.8.0) + slop (3.4.7) + sprockets (2.2.2) hike (~> 1.2) + multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sqlite3 (1.3.4) - term-ansicolor (1.0.7) - thor (0.14.6) - tilt (1.3.3) - treetop (1.4.10) + sqlite3 (1.3.8) + term-ansicolor (1.3.0) + tins (~> 1.0) + thor (0.18.1) + tilt (1.4.1) + tins (1.0.0) + treetop (1.4.15) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.30) - uglifier (1.0.4) + tzinfo (0.3.38) + uglifier (2.4.0) execjs (>= 0.3.0) - multi_json (>= 1.0.2) - unicorn (4.1.1) - kgio (~> 2.4) + json (>= 1.8.0) + unicorn (4.8.2) + kgio (~> 2.6) rack - raindrops (~> 0.6) - warden (1.0.6) + raindrops (~> 0.7) + warden (1.2.3) rack (>= 1.0) - will_paginate (3.0.2) - xpath (0.1.4) + will_paginate (3.0.5) + xpath (2.0.0) nokogiri (~> 1.3) PLATFORMS @@ -234,28 +251,34 @@ PLATFORMS DEPENDENCIES bson_ext cancan - coffee-rails (~> 3.1.0) + capybara + coffee-rails + coveralls cucumber-rails database_cleaner - devise (>= 1.2) + debugger + devise (~> 2.2.2) fabrication faker + hackety_hack-lessons (~> 1.1) haml-rails inherited_resources - jnunemaker-validatable (>= 1.8.4) jquery-rails json - mm-devise (>= 1.2) + launchy + letter_opener + mm-devise (~> 2.0) mocha mongo_mapper pg pry - rails (= 3.1.0) - rdiscount + rails (= 3.2.17) + redcarpet (~> 3.0) rspec-rails - sass-rails (~> 3.1.0) + sass-rails semantic_menu! simple_form! + simplecov sqlite3 uglifier unicorn diff --git a/README.md b/README.md index f4dbd2de..d9ae1f62 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,48 @@ -# hackety-hack.com +# hackety-hack.com # -[![Build status](https://secure.travis-ci.org/hacketyhack/hackety-hack.com.png)](http://travis-ci.org/#!/hacketyhack/hackety-hack.com) +[![Build status](https://img.shields.io/travis/hacketyhack/hackety-hack.com.svg)](http://travis-ci.org/#!/hacketyhack/hackety-hack.com)[![Code Climate](https://img.shields.io/codeclimate/github/hacketyhack/hackety-hack.com.svg)](https://codeclimate.com/github/hacketyhack/hackety-hack.com)[![Coverage Status](https://img.shields.io/coveralls/hacketyhack/hackety-hack.com/master.svg)](https://coveralls.io/r/hacketyhack/hackety-hack.com?branch=master) -This is the source code for hackety-hack.com. +[hackety-hack.com][hh.com] is the web backend powering the collaborative features of [Hackety Hack][hh]. It is written in Rails 3 and continues to be under active development (see 'Helping Out'), the switch to Rails 3 happened quite recently so there's still lots to be done. -At the moment, we're re-building it for Rails 3. So there's not a lot of docs. Sorry about that! +### Helping Out ### + +If you have any experience writing Rails apps, feel free to help out, we're open to pull requests as long as you follow a few conditions. + ++ **Test your code**, we really can't stress this enough, ideally you should be practicing [TDD][tdd] and writing tests before you even write your code. If you don't test your code, we have no way of knowing if it works properly so please do test. ++ **If it's a major feature, file an issue**, if you file an issue we can discuss certain aspects of the new feature with you and ensure it's a good fit for hackety-hack.com. + +## Translations ## + +Hello everyone! + +We are in the process of translating Hackety Hack into as many foreign languages as possible so that people around the world would be able to use the site with ease. If you are bilingual and interested in helping us make Hackety Hack a truly global phenomenon, accessible by all regardless of location or nationality, and make learning Ruby even more fun! then please sign up here http://crowdin.net/project/hackety-hackcom/invite + +## Getting Started ## + +Once you've cloned this repository, running `script/bootstrap` should tell you everything you need to know. + +In case you prefer gems to be managed entirely by bundler run this before bootstrapping: + + bundle config --global path .bundle + bundle config --global binstubs bin + export PATH="$PWD/bin:$PATH" + +Be careful with using `bin` in `$PATH` it is very risky when used with public projects. + +Dependencies for the curious: + +- Ruby: 1.9.3 is preferred. +- MongoDB: 2.2.x or 2.4.x + +As long as you have those things, the script will handle the rest as best it can, including installing the gem dependencies with Bundler. + +If the tests aren't passing when you clone, open [an issue][issues] or drop into +[#hacketyhack on freenode][irc]. + +Additionally, if you're _not_ a developer and you have a feature you'd really like to see on the site, file [an issue][issues] and we'll be sure to look into it on your behalf. + +[hh.com]: http://hackety-hack.com/ +[hh]: https://github.com/hacketyhack/hacketyhack +[irc]: http://webchat.freenode.net/?channels=#hacketyhack +[issues]: https://github.com/hacketyhack/hackety-hack.com/issues +[tdd]: http://en.wikipedia.org/wiki/Test-driven_development diff --git a/app/assets/images/feed_icon.png b/app/assets/images/feed_icon.png new file mode 100644 index 00000000..d384eac7 Binary files /dev/null and b/app/assets/images/feed_icon.png differ diff --git a/app/assets/javascripts/blog.js.coffee b/app/assets/javascripts/blog.js.coffee new file mode 100644 index 00000000..76156794 --- /dev/null +++ b/app/assets/javascripts/blog.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/lessons.js.coffee b/app/assets/javascripts/lessons.js.coffee new file mode 100644 index 00000000..76156794 --- /dev/null +++ b/app/assets/javascripts/lessons.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 42daa395..39a9a87a 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -13,4 +13,9 @@ @import "forms.css.scss"; @import "content.css.scss"; @import "home.css.scss"; -@import "questions.css.scss"; \ No newline at end of file +@import "questions.css.scss"; +@import "programs.css.scss"; +@import "users.css.scss"; +@import "lessons.css.scss"; +@import "blog.css.scss"; + diff --git a/app/assets/stylesheets/blog.css.scss b/app/assets/stylesheets/blog.css.scss new file mode 100644 index 00000000..3b293afe --- /dev/null +++ b/app/assets/stylesheets/blog.css.scss @@ -0,0 +1,29 @@ +.post.teaser { + h2.title { + line-height: 1; + } + + border-bottom: 1px solid #ccc; + margin-bottom: 3em; +} + +.post { + + padding-bottom: 2em; + + .created_at { + margin-bottom: 1em; + color: #888; + font-size: 1.15em; + font-style: italic; + + } + + margin-right: 3em; + .content { + p { + font-size: 1.15em; + line-height: 1.75em; + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/content.css.scss b/app/assets/stylesheets/content.css.scss index 98c32454..89192c0a 100644 --- a/app/assets/stylesheets/content.css.scss +++ b/app/assets/stylesheets/content.css.scss @@ -1,18 +1,62 @@ p { - margin: 0; - padding-bottom: 0.5em; + margin-bottom: 1em; +} + +h1 { + +} + +h2 { + font-size: 1.75em; + margin-bottom: 0.5em; +} + +.heading { + @include clearfix; + padding: 0.75em; + background: $green; + + h3, .description { + color: white; + } + + h3 { + float: left; + margin-bottom: 0; + margin-right: 1em; + } + .description { + float: left; + margin-top: 0.2em; + } +} + + +.block-message { + margin-bottom: 0.5em; + p { + margin-bottom: 1em; + } } -#faq { - h2 { - line-height: 1.15; - padding: 0.5em 0; - margin: 0.5em 0; - border-bottom: 1px solid #ccc; - border-top: 1px solid #ccc; +ul.inline-menu { + @include clearfix; + list-style-type: none; + margin: 0; + li { + float: left; + padding-right: 1em; + border-right: 1px solid #ccc; + margin-right: 1em; } - h1 { - margin-top: 1em; - color: $heading; + li:last-child { + border: none; + margin: 0; + padding: 0; } +} + +.bottom-nav { + border-top: 1px solid #ccc; + padding-top: 1.5em; } \ No newline at end of file diff --git a/app/assets/stylesheets/home.css.scss b/app/assets/stylesheets/home.css.scss index d47f45ec..8a7eff79 100644 --- a/app/assets/stylesheets/home.css.scss +++ b/app/assets/stylesheets/home.css.scss @@ -23,13 +23,13 @@ .container { padding-top: 0; } - + border-top: none; section { @include clearfix; background: white; padding-bottom: 1em; - + p { font-size: 1.25em; line-height: 1.75em; @@ -45,7 +45,7 @@ } } - + #download { background: white url("download.png") 0 4em no-repeat; h1 { @@ -58,7 +58,7 @@ text-align: center; margin-top: 1em; margin-bottom: 1em; - + .download { text-transform: uppercase; margin-bottom: 1em; @@ -72,7 +72,7 @@ @include offset(5); } } - + #explore { background: white url("explore.png") 95% 7em no-repeat; h1 { @@ -84,7 +84,7 @@ margin-left: 2em; } } - + #ask { background: white url("ask.png") 0 7em no-repeat; min-height: 340px; @@ -98,41 +98,4 @@ @include offset(5); } } -} - -/*.body-home #content-wrap { - border-top: none;*/ -/* section { - @include clearfix; - margin: 0 36px 36px 18px; - padding-bottom: 36px; - border-bottom: 6px dotted $linkColor; - - font-size: 1.7em; - - h1 { - font-size: 1.6em; - } - p { - font-size: 1em; - line-height: 1.5em; - margin-bottom: 1em; - } - - img.new_program { - @include columns(5); - } - img.lesson { - @include columns(8); - float: right; - margin-right: 0; - margin-left: $gridGutterWidth*2; - - } - } - - section.last { - border: none; - margin-bottom: 0; - } -}*/ +} \ No newline at end of file diff --git a/app/assets/stylesheets/layout.css.scss b/app/assets/stylesheets/layout.css.scss index b73cd725..9c52f058 100644 --- a/app/assets/stylesheets/layout.css.scss +++ b/app/assets/stylesheets/layout.css.scss @@ -3,29 +3,35 @@ html { } body { + min-width: 976px; + color: $text-color; background: $background url("square_bg.png") fixed; } +p,h1,h2,h3,h4,h5 { + color: $text-color; +} + .topbar { - position: relative; + position: relative; height: auto; @include box-shadow(0px 1px 10px rgba(0,0,0,0.5)); border-bottom: 1px solid $heading + #444; } -header .topbar-inner { +header .topbar-inner { background: $heading; .container { padding: 0.6em 0; } - + li a { border-radius: 8px; font-size: 14px; padding: 11px 11px 10px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); } - + li a:hover { background-color: $heading + #111; text-shadow: none; @@ -36,7 +42,7 @@ header .topbar-inner { margin-left: -40px; width: 185px; height: 40px; - + a { position: absolute; top: -2px; @@ -53,13 +59,13 @@ header .topbar-inner { #content-wrap { border-top: 1px solid ($heading + #444); margin: auto; - + .container { position: relative; - } + } } -#content-wrap > .container { +#content-wrap > .container { background: white; border-radius: 0 0 8px 8px; padding: 40px 18px 18px; @@ -67,10 +73,35 @@ header .topbar-inner { } .two-column { - #content { + .page-title, #content { @include columns(11); + h1.title { + float: left; + } + .feed { + float: right; + margin-top: 5px; + + a { + display: block; + height: 24px; + background: url('feed_icon.png') no-repeat 6px 4px; + color: #4c8acd; + font-size: 11px; + text-align: right; + border: 1px solid #eee; + line-height: 25px; + padding: 0 8px 0 25px; + @include border-radius(3px); + + &:hover { + background: #f3f3f3 url('feed_icon.png') no-repeat 6px 4px; + text-decoration: none; + } + } + } } - + #sidebar { @include columns(5, true); .edit, .delete { @@ -81,15 +112,33 @@ header .topbar-inner { background: #f3f3f3; margin-bottom: 8px; padding: 1.5em; + + a { + color: darken($blue, 10%); + } + + a.btn { + color: white; + } + + ul { + margin: 0; + li { + list-style-type: none; + margin-bottom: 0.5em; + border-bottom: 1px solid #ddd; + padding-bottom: 0.5em; + } + li:last-child { + border: none; + } + } } } - -} -p, h1,h2,h3,h4,h5 { - color: #555; } + .alert-message { margin-top: -18px; margin-bottom: 30px; @@ -98,7 +147,7 @@ p, h1,h2,h3,h4,h5 { footer { background: $heading; padding: 0.5em 0; - + .pills { margin: 0; a { @@ -108,5 +157,5 @@ footer { a:hover { background-color: #444; } - } -} \ No newline at end of file + } +} diff --git a/app/assets/stylesheets/lessons.css.scss b/app/assets/stylesheets/lessons.css.scss new file mode 100644 index 00000000..20d8b18c --- /dev/null +++ b/app/assets/stylesheets/lessons.css.scss @@ -0,0 +1,82 @@ +/* Lessons menu */ +ul#lessons { + list-style-type: none; + margin: 0; + > li { + @include clearfix; + padding-bottom: 1em; + border-bottom: 1px solid #ccc; + margin-bottom: 1em; + .info { + @include columns(7); + .title { + font-size: 1.1em; + font-weight: bold; + margin-bottom: 0.2em; + } + } + .categories { + @include columns(4, last); + li { + float: right; + } + } + } +} + +/* Lesson content styles */ +#content-wrap.lesson { + .page-title { + margin-left: 3em; + } + #lesson-content { + padding-left: 3em; + max-width: 500px; + + h2 { + margin-top: 0.75em; + border-top: 1px solid #ccc; + padding-top: 0.5em; + } + + img { + @include border-radius; + @include box-shadow; + border: 1px solid #ccc; + padding: 0.75em; + } + } +} + +/* Category pills */ +ul.lesson-categories { + @include clearfix; + list-style-type: none; + margin: 0; + font-size: 0.9em; + + li { + float: left; + @include border-radius(50px); + padding: 0.25em 0.75em; + margin-right: 0.5em; + background: #ddd; + border: 1px solid #ccc; + } + + .beginner { + background-color: $green; + border-color: darken($green, 10%); + color: white; + } + .hackety { + background-color: $blue; + border-color: darken($blue, 10%); + color: white; + } + .shoes { + background-color: $orangered; + border-color: darken($orangered, 10%); + color: white; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/lib/bootstrap/patterns.scss b/app/assets/stylesheets/lib/bootstrap/patterns.scss index 081eab04..2a6ae7fd 100644 --- a/app/assets/stylesheets/lib/bootstrap/patterns.scss +++ b/app/assets/stylesheets/lib/bootstrap/patterns.scss @@ -603,9 +603,10 @@ input[type=submit].btn { li { display: inline; } - a { + a, em, .disabled { float: left; padding: 0 14px; + font-style: normal; line-height: ($baseline * 2) - 2; border-right: 1px solid; border-right-color: #ddd; diff --git a/app/assets/stylesheets/lib/bootstrap/preboot.scss b/app/assets/stylesheets/lib/bootstrap/preboot.scss index 0c75a7fc..82af2bae 100644 --- a/app/assets/stylesheets/lib/bootstrap/preboot.scss +++ b/app/assets/stylesheets/lib/bootstrap/preboot.scss @@ -131,7 +131,7 @@ $analog2: spin($baseColor, -22); width: ($gridColumnWidth * $columnSpan) + ($gridGutterWidth * ($columnSpan - 1)); display: inline; float: left; - + @if $last { margin-right: 0; } @@ -203,7 +203,7 @@ $analog2: spin($baseColor, -22); background-image: -webkit-linear-gradient(left, $startColor, $endColor); // Safari 5.1+, Chrome 10+ background-image: -o-linear-gradient(left, $startColor, $endColor); // Opera 11.10 background-image: linear-gradient(left, $startColor, $endColor); // Le standard - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$startColor}', endColorstr='#{$endColor}', GradientType=1); // IE9 and down + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$startColor}', endColorstr='#{$endColor}', GradientType=1); // IE9 and down } @mixin gradient-vertical ($startColor: #555, $endColor: #333) { background-color: $endColor; @@ -215,7 +215,7 @@ $analog2: spin($baseColor, -22); background-image: -webkit-linear-gradient(top, $startColor, $endColor); // Safari 5.1+, Chrome 10+ background-image: -o-linear-gradient(top, $startColor, $endColor); // Opera 11.10 background-image: linear-gradient(top, $startColor, $endColor); // The standard - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$startColor}', endColorstr='#{$endColor}', GradientType=0); // IE9 and down + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$startColor}', endColorstr='#{$endColor}', GradientType=0); // IE9 and down } @mixin gradient-directional ($startColor: #555, $endColor: #333, $deg: 45deg) { background-color: $endColor; @@ -235,7 +235,7 @@ $analog2: spin($baseColor, -22); background-image: -ms-linear-gradient($startColor, $midColor $colorStop, $endColor); background-image: -o-linear-gradient($startColor, $midColor $colorStop, $endColor); background-image: linear-gradient($startColor, $midColor $colorStop, $endColor); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$startColor}', endColorstr='#{$endColor}', GradientType=0); // IE9 and down, gets no color-stop at all for proper fallback + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$startColor}', endColorstr='#{$endColor}', GradientType=0); // IE9 and down, gets no color-stop at all for proper fallback } // Gradient Bar Colors for buttons and allerts diff --git a/app/assets/stylesheets/lib/shared.css.scss b/app/assets/stylesheets/lib/shared.css.scss index 159ce7a3..679cc2d0 100644 --- a/app/assets/stylesheets/lib/shared.css.scss +++ b/app/assets/stylesheets/lib/shared.css.scss @@ -1,5 +1,6 @@ $heading: #253E59; $background: $heading + #999; +$text-color: #555; $orangered: #BF4904; diff --git a/app/assets/stylesheets/programs.css.scss b/app/assets/stylesheets/programs.css.scss new file mode 100644 index 00000000..84b3a897 --- /dev/null +++ b/app/assets/stylesheets/programs.css.scss @@ -0,0 +1,36 @@ +.programs { + + margin-bottom: 2em; + + ul { + @include clearfix; + margin: 0; + padding-top: 1em; + + li { + @include columns(5); + padding: 0.75em 0; + border-top: 1px solid #ddd; + + .title { + font-weight: bold; + margin-bottom: 0.1em; + } + + .author { + a, & { + color: lighten($text-color, 40%); + } + } + } + li:nth-child(3n+1) { + clear: both; + } + } +} + +#featured { + .heading { + background: $orangered; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/questions.css.scss b/app/assets/stylesheets/questions.css.scss index 592368d6..67a61796 100644 --- a/app/assets/stylesheets/questions.css.scss +++ b/app/assets/stylesheets/questions.css.scss @@ -4,7 +4,7 @@ margin-bottom: 0.15em; } .meta { - a { + a { font-style: italic; } font-style: italic; @@ -32,10 +32,10 @@ } @include columns(1); padding: 6px - } + } .summary { @include columns(9, last); - + h2 { line-height: 1; margin-bottom: 0.15em; @@ -45,7 +45,7 @@ } } } - + .unanswered { .answer-count, .answer-count h3 { color: $orangered; @@ -59,17 +59,16 @@ .accepted { background-image: url("accepted.png"); } - + } .question { - color: #555; p { font-size: 1.15em; line-height: 1.5; margin-bottom: 1.25em; } - + h2 { margin-top: 0.5em; padding-bottom: 0.5em; @@ -83,8 +82,8 @@ position: relative; list-style-type: none; padding: 1em 1em; - border-bottom: 1px solid #ddd; - + border-bottom: 1px solid #ddd; + .choose { float: left; opacity: 0.5; @@ -106,5 +105,10 @@ } .selected-answer { background: rgba(158, 183,68, 0.1) url("accepted.png") 99% 0.5em no-repeat; - } + } +} + +.move_question { + float:right; + padding-top:4px; } diff --git a/app/assets/stylesheets/users.css.scss b/app/assets/stylesheets/users.css.scss new file mode 100644 index 00000000..cd972aa5 --- /dev/null +++ b/app/assets/stylesheets/users.css.scss @@ -0,0 +1,38 @@ +.user-questions, .user-answers { + ul { + margin: 0; + list-style-type: none; + padding: 1em 0; + + li { + @include clearfix; + padding-bottom: 0.5em; + border-bottom: 1px solid #ddd; + margin-bottom: 0.5em; + + .title, .description { + @include columns(8); + } + + .meta { + @include columns(3, true); + text-align: right; + } + } + } +} + +.title { + display: inline; + margin-right: 10px; + margin: 0px; +} + +#user-list { + li { + list-style-type: none; + padding-bottom: 3px; + padding-top: 3px; + } +} + diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index b03d47ad..0761042d 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -7,8 +7,17 @@ def create @answer = Answer.create params[:answer] @answer.question = @question @answer.user = current_user + + Notification.new_answer(@question).deliver + create!(:notice => "Answer Posted!"){ question_url(params[:question_id]) } end - + def update + # inherited resorces (gem) magic + super do |format| + format.html { redirect_to question_url(resource.question) } + end + end end + diff --git a/app/controllers/api/rels_controller.rb b/app/controllers/api/rels_controller.rb index 3cc30f1f..00894e2e 100644 --- a/app/controllers/api/rels_controller.rb +++ b/app/controllers/api/rels_controller.rb @@ -6,4 +6,4 @@ def index def show @rel = Rel.first(:slug => params[:id]) end -end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 95596f7c..68f1059c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,14 +1,7 @@ class ApplicationController < ActionController::Base protect_from_forgery - helper_method :title - def title t=nil - @title = t unless t.blank? - @title - end - rescue_from CanCan::AccessDenied do |exception| redirect_to login_url, :alert => exception.message end - end diff --git a/app/controllers/blog_controller.rb b/app/controllers/blog_controller.rb new file mode 100644 index 00000000..7f06d3be --- /dev/null +++ b/app/controllers/blog_controller.rb @@ -0,0 +1,23 @@ +class BlogController < ApplicationController + before_filter :authenticate_user!, :only => [:admin, :create] + + def index + @posts = BlogPost.all.reverse + end + + def show + @post = BlogPost.find(params[:id]) + end + + def admin + redirect_to blog_index_path unless current_user.blog_poster + @post = BlogPost.new + @posts = BlogPost.all.reverse + end + + def create + redirect_to blog_index_path and return unless current_user.blog_poster + @post = BlogPost.create(params[:blog_post]) + redirect_to admin_blog_index_path, :notice => "Blog Post created!" + end +end diff --git a/app/controllers/lessons_controller.rb b/app/controllers/lessons_controller.rb new file mode 100644 index 00000000..3f3ac02c --- /dev/null +++ b/app/controllers/lessons_controller.rb @@ -0,0 +1,9 @@ +class LessonsController < ApplicationController + def index + @lessons = HacketyHack::Lessons.all + end + + def show + @lesson = HacketyHack::Lessons.find_by_slug(params[:id]) + end +end diff --git a/app/controllers/mailer_controller.rb b/app/controllers/mailer_controller.rb new file mode 100644 index 00000000..19e8eaba --- /dev/null +++ b/app/controllers/mailer_controller.rb @@ -0,0 +1,22 @@ +class MailerController < ApplicationController + load_and_authorize_resource class: Message + + def new + @users = Array(params[:user]) + @emails = User.where(:username => @users).all.map(&:email) + @message = Message.new + end + + def create + @message = Message.new(params[:message]) + + if @message.valid? + @message.email.each do |email| + MessageMailer.new_message(@message, email).deliver + end + redirect_to users_index_path, :notice => "Email sent correctly" + else + render :new, notice: "There was an error" + end + end +end diff --git a/app/controllers/programs_controller.rb b/app/controllers/programs_controller.rb index fcaadd71..f4e306df 100644 --- a/app/controllers/programs_controller.rb +++ b/app/controllers/programs_controller.rb @@ -4,9 +4,9 @@ class ProgramsController < InheritedController belongs_to :user, :optional => true respond_to :html, :only => [:index, :show] - def show - # @program = Program.find_by_slug(params[:slug]) - show! + def index + @featured = Program.featured + index! end ################# diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 36b4f4ba..8038f468 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -1,6 +1,9 @@ class QuestionsController < InheritedController - before_filter :set_support load_and_authorize_resource + prepend_before_filter :set_presenter + prepend_before_filter :set_support + + respond_to :atom, :only => :index def create @question = Question.create params[:question] @@ -18,33 +21,50 @@ def update end def collection - if @support - @questions ||= end_of_association_chain.supports.newest_first.paginate(:page => params[:page]) - else - @questions ||= end_of_association_chain.no_supports.newest_first.paginate(:page => params[:page]) - end + @questions = @presenter.apply_scope(end_of_association_chain).newest_first.paginate(:page => params[:page]) end - + def collection_url - @support ? support_questions_path : questions_path + collection_path end + + def collection_path + @presenter.collection_path + end + def new_resource_path - @support ? new_support_question_path : new_question_path + @presenter.new_resource_path end - - def resource_path(*resource) - @support ? support_question_path(*resource) : question_path(*resource) + + def resource_url(*params) + resource_path(params) end - + + def resource_path(*other) + if other[0] + @presenter.resource_path(other) + else + @presenter.resource_path(resource) + end + end + def edit_resource_path @support ? edit_support_question_path : edit_question_path end - + def set_support - @support = request.env['PATH_INFO'].include?('support') + @support = request.env['PATH_INFO'].include?('support') || params[:support] if @support && params[:question] params[:question][:support] = true end end - + + def set_presenter + if @support + @presenter = SupportPresenter.new(resource) + else + @presenter = QuestionPresenter.new(resource) + end + end + end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index e34ded3e..60acc47c 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -23,7 +23,10 @@ def download @other_platforms = ["mac", "windows", "linux"] - [@platform] end - def api_root; render :layout => "api"; end + def api_root + render :layout => "api" + end + def newest_version render :json => {:version => "1.0.0"} end @@ -31,10 +34,6 @@ def newest_version protected def platform - if Rails.env.test? - "mac" - else - request.user_agent.match(/Mac|Linux|Windows/).try(:[], 0).try(:downcase) - end + request.user_agent.nil? ? nil : request.user_agent.downcase end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 185f5ee6..b53d2ba9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,12 @@ class UsersController < InheritedController load_and_authorize_resource - skip_authorize_resource :only => [:following, :followers] #anyone can perform these read-only actions + skip_authorize_resource :only => [:following, :followers, :deleted_user] #anyone can perform these read-only actions + require 'will_paginate/array' + + def index + redirect_to root_path unless can? :manage, @users + @users = User.all.paginate(:page => params[:page], :per_page => 10) + end def follow followee = User.first(:id => params[:user][:followee]) @@ -23,11 +29,15 @@ def unfollow end def following - @user = User.first(:id => params[:user_id]) + @user = User.first(:username => params[:user_id]) end def followers - @user = User.first(:id => params[:user_id]) + @user = User.first(:username => params[:user_id]) + end + + def deleted_user + @user = DeletedUser.new end ################# @@ -35,5 +45,6 @@ def followers def resource @user ||= end_of_association_chain.find_by_username(params[:id]) end - + end + diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7ad5d685..eb35501b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,5 +1,5 @@ module ApplicationHelper - def program_path program - "/users/#{program.author_username}/programs/#{program.slug}" + def markdown(text) + Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(hard_wrap: true), autolink: true).render(text).html_safe end end diff --git a/app/helpers/blog_helper.rb b/app/helpers/blog_helper.rb new file mode 100644 index 00000000..77a397e9 --- /dev/null +++ b/app/helpers/blog_helper.rb @@ -0,0 +1,2 @@ +module BlogHelper +end diff --git a/app/helpers/lessons_helper.rb b/app/helpers/lessons_helper.rb new file mode 100644 index 00000000..f024005f --- /dev/null +++ b/app/helpers/lessons_helper.rb @@ -0,0 +1,11 @@ +module LessonsHelper + + def lesson_categories lesson + output = content_tag :ul, :class => "lesson-categories" do + lesson.metadata["categories"].reduce('') { |c, x| + c << content_tag(:li, x, :class => x) + }.html_safe + end + end + +end diff --git a/app/helpers/programs_helper.rb b/app/helpers/programs_helper.rb new file mode 100644 index 00000000..3ec6d832 --- /dev/null +++ b/app/helpers/programs_helper.rb @@ -0,0 +1,21 @@ +module ProgramsHelper + def program_link program + if program.author_username.present? + "#{link_to(program.title.titleize, user_program_path(program.author_username, program))}".html_safe + else + "#{link_to(program.title.titleize, url_for(:controller => '/programs', :action => 'show', :id => program.slug))}".html_safe + end + end + + def author_link username, program = nil + return nil unless username + output = "" + output += link_to(username, user_path(username)) + + if program + output += ", #{program.created_at.strftime('%m/%d/%y')}" unless program.created_at.nil? + end + + output.html_safe + end +end diff --git a/app/mailers/message_mailer.rb b/app/mailers/message_mailer.rb new file mode 100644 index 00000000..790550b3 --- /dev/null +++ b/app/mailers/message_mailer.rb @@ -0,0 +1,9 @@ +class MessageMailer < ActionMailer::Base + default from: "steve@hackety.com" + + def new_message message, email + @message = message + @email = email + mail(:to => @email, :subject => @message.subject) + end +end diff --git a/app/mailers/notification.rb b/app/mailers/notification.rb new file mode 100644 index 00000000..24e91128 --- /dev/null +++ b/app/mailers/notification.rb @@ -0,0 +1,10 @@ +class Notification < ActionMailer::Base + default from: "steve@hackety.com" + default_url_options[:host] = "hackety.com" + + def new_answer(question) + @question = question + @url = question_url(question, :host => "hackety.com") + mail(:to => question.user.email, :subject => "New Answer on Hackety.com!") + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index ca0d984e..16b37006 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -4,7 +4,11 @@ class Ability def initialize(user) user ||= User.new - can :read, :all + if user.moderator? + can :manage, :all + else + can :read, :all + end unless user.new_record? can :create, [Question, Answer] diff --git a/app/models/answer.rb b/app/models/answer.rb index e5d27c3b..bfed4f8a 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -2,6 +2,7 @@ class Answer include MongoMapper::Document key :description, String + timestamps! validates_presence_of :description diff --git a/app/models/blog_post.rb b/app/models/blog_post.rb new file mode 100644 index 00000000..5b50f9b6 --- /dev/null +++ b/app/models/blog_post.rb @@ -0,0 +1,8 @@ +class BlogPost + include MongoMapper::Document + + key :title, String, :required => true + key :content, String, :required => true + + timestamps! +end diff --git a/app/models/deleted_user.rb b/app/models/deleted_user.rb new file mode 100644 index 00000000..373029ed --- /dev/null +++ b/app/models/deleted_user.rb @@ -0,0 +1,13 @@ +class DeletedUser + def username + 'Deleted User' + end + + def email + 'none' + end + + def to_param + 'deleted_user' + end +end diff --git a/app/models/message.rb b/app/models/message.rb new file mode 100644 index 00000000..2c00d8ba --- /dev/null +++ b/app/models/message.rb @@ -0,0 +1,11 @@ +class Message + include MongoMapper::Document + key :email, Array + key :subject, String + key :body, String + validates_presence_of :email, :body + + def to_param + {email: email, subject: subject, body: body} + end +end \ No newline at end of file diff --git a/app/models/program.rb b/app/models/program.rb index 29b095b8..3fb65f5e 100644 --- a/app/models/program.rb +++ b/app/models/program.rb @@ -4,9 +4,13 @@ class Program key :author_username, String key :slug, String key :title, String + key :description, String key :source_code, String + key :featured, Boolean + timestamps! scope :by_username, lambda { |username| where(:author_username => username) } + scope :featured, where(:featured => true) before_create :make_slug @@ -21,4 +25,5 @@ def to_param slug end + end diff --git a/app/models/question.rb b/app/models/question.rb index f57ecbf6..70ea09fd 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -14,5 +14,14 @@ class Question scope :newest_first, sort(:created_at.desc) scope :supports, where(:support => true ) - scope :no_supports, where(:support => nil) + scope :no_supports, where('$or' => [{:support=> false}, {:support => nil}]) + + def user + asker = super + if asker.nil? + DeletedUser.new + else + asker + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 695107a7..d03c5668 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,26 @@ class User key :username, String key :email, String key :about, String + key :moderator, Boolean + key :blog_poster, Boolean + + ## Database authenticatable + key :email, :type => String, :null => false + key :encrypted_password, :type => String, :null => false + + ## Recoverable + key :reset_password_token, :type => String + key :reset_password_sent_at, :type => Time + + ## Rememberable + key :remember_created_at, :type => Time + + ## Trackable + key :sign_in_count, :type => Integer + key :current_sign_in_at, :type => Time + key :last_sign_in_at, :type => Time + key :current_sign_in_ip, :type => String + key :last_sign_in_ip, :type => String many :questions many :answers @@ -16,6 +36,8 @@ class User :message => "Make your username from letters, numbers, underscores('_'), and dots('.')." validates_length_of :username, :in => (1..32), :message => "Your username needs at least 1 character but no more than 32." + validates_uniqueness_of :username, + :message => "User name has been taken already. Please try another" def to_param self.username diff --git a/app/presenters/question_presenter.rb b/app/presenters/question_presenter.rb new file mode 100644 index 00000000..265a653e --- /dev/null +++ b/app/presenters/question_presenter.rb @@ -0,0 +1,35 @@ +class QuestionPresenter + include Rails.application.routes.url_helpers + + def initialize(q) + @question = q + end + + def resource_path(question) + if question + question_path(question) + else + question_path(@question) + end + end + + def new_resource_path + new_question_path + end + + def collection_path + questions_path + end + + def edit_resource_path + edit_question_path(@question) + end + + def apply_scope(chain) + chain.no_supports + end + + def answers_path + question_answers_path(@question) + end +end diff --git a/app/presenters/support_presenter.rb b/app/presenters/support_presenter.rb new file mode 100644 index 00000000..acb871c5 --- /dev/null +++ b/app/presenters/support_presenter.rb @@ -0,0 +1,35 @@ +class SupportPresenter + include Rails.application.routes.url_helpers + + def initialize(sq) + @support_question = sq + end + + def page_title + "Support Questions" + end + + def new_resource_path + new_support_question_path + end + + def collection_path + support_questions_path + end + + def edit_resource_path + edit_support_question_path(@support_question) + end + + def resource_path(question) + if question + support_question_path(question) + else + support_question_path(@support_question) + end + end + + def apply_scope(chain) + chain.supports + end +end diff --git a/app/views/answers/_form.html.haml b/app/views/answers/_form.html.haml index 964a3635..53549aa9 100644 --- a/app/views/answers/_form.html.haml +++ b/app/views/answers/_form.html.haml @@ -1,8 +1,3 @@ -= simple_form_for(@answer, :url => question_answers_path(@question)) do |f| - = f.error_notification += simple_form_for(resource, :url => @form_url) do |f| + = render 'form_content', :f => f - .inputs - = f.input :description, :as => :text, :label => "Answer" - - .actions - = f.button :submit, :value => "Post Answer", :class => "primary btn" diff --git a/app/views/answers/_form_content.html.haml b/app/views/answers/_form_content.html.haml new file mode 100644 index 00000000..9974bd1e --- /dev/null +++ b/app/views/answers/_form_content.html.haml @@ -0,0 +1,9 @@ += f.error_notification + +.inputs + = f.input :description, :as => :text, :label => "Answer", + :hint => "You can format your input with Markdown".html_safe + +.actions + = f.button :submit, :value => "Post Answer", :class => "primary btn" + diff --git a/app/views/answers/_list.html.haml b/app/views/answers/_list.html.haml index aa395075..41ddaea3 100644 --- a/app/views/answers/_list.html.haml +++ b/app/views/answers/_list.html.haml @@ -3,8 +3,7 @@ .meta #{link_to list.user.username, user_path(list.user)} says .description - :markdown - #{list.description} + =markdown(list.description) .links - if can? :update, list diff --git a/app/views/answers/_short_answer.html.haml b/app/views/answers/_short_answer.html.haml new file mode 100644 index 00000000..33d8b788 --- /dev/null +++ b/app/views/answers/_short_answer.html.haml @@ -0,0 +1,3 @@ += simple_form_for(@answer, :url => question_answers_path(@question)) do |f| + = render 'answers/form_content', :f => f + diff --git a/app/views/answers/edit.html.haml b/app/views/answers/edit.html.haml index e05dd7e9..53544a89 100644 --- a/app/views/answers/edit.html.haml +++ b/app/views/answers/edit.html.haml @@ -1,4 +1,5 @@ - content_for :title do - = "Editing #{resource}" + = "Editing your answer" = render 'form' + diff --git a/app/views/blog/admin.html.haml b/app/views/blog/admin.html.haml new file mode 100644 index 00000000..03c9bea9 --- /dev/null +++ b/app/views/blog/admin.html.haml @@ -0,0 +1,16 @@ +%h1 Blog Admin + +%h2 New Post += simple_form_for @post, :url => blog_index_path do |f| + .inputs + = f.input :title + = f.input :content, :as => :text + + .actions + = f.button :submit, :value => "Create Blog post", :class => "primary btn" + +%h2 All Posts +%ul + - @posts.each do |post| + %li= post.title + diff --git a/app/views/blog/index.html.haml b/app/views/blog/index.html.haml new file mode 100644 index 00000000..aa4ebada --- /dev/null +++ b/app/views/blog/index.html.haml @@ -0,0 +1,17 @@ +- content_for :title do + The Hackety Blog! + +- content_for :sidebar do + = render :partial => "shared/ask" + = render :partial => "shared/lessons" + +- @posts.each do |post| + .post.teaser + %h2.title= link_to post.title, blog_path(post) + + .created_at + = post.created_at.strftime("Posted %B %d, %Y at %l:%M %p") + + .content + =markdown(post.content) + diff --git a/app/views/blog/show.html.haml b/app/views/blog/show.html.haml new file mode 100644 index 00000000..cd66825c --- /dev/null +++ b/app/views/blog/show.html.haml @@ -0,0 +1,14 @@ +- content_for :title do + = @post.title + +- content_for :sidebar do + = render :partial => "shared/recent" + = render :partial => "shared/ask" + = render :partial => "shared/lessons" + +.post + .created_at + = @post.created_at.strftime("Posted %B %d, %Y at %l:%M %p") + + .content + =markdown(@post.content) diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb deleted file mode 100644 index 310baf93..00000000 --- a/app/views/devise/registrations/edit.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<% content_for :title do %> -Edit <%= resource_name.to_s.humanize %> -<% end %> - -<%= simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> - <%= f.error_notification %> - -
- <%= f.input :username, :autofocus => true %> - <%= f.input :email, :autofocus => true %> - <%= f.input :password, :hint => "leave it blank if you don't want to change it", :required => false %> - <%= f.input :password_confirmation, :required => false %> - <%= f.input :current_password, :hint => "we need your current password to confirm your changes", :required => true %> - <%= f.input :about %> -
- -
- <%= f.button :submit, "Update" %> -
-<% end %> - -

Cancel my account

- -

Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.

- -<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml new file mode 100644 index 00000000..d945daa0 --- /dev/null +++ b/app/views/devise/registrations/edit.html.haml @@ -0,0 +1,10 @@ +- content_for :title do + Edit #{resource_name.to_s.humanize} + += render :partial => "users/form", :locals => {:submit_label => "Update"} + +- content_for :sidebar do + %section + %h2 Cancel my account + %p + Unhappy? #{link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete}. diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 65d498fa..0167dca8 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -2,6 +2,6 @@ Sign Up <% end %> -<%= render :partial => "users/form" %> +<%= render :partial => "users/form", :locals => {:submit_label => "Sign up"} %> <%= render :partial => "devise/shared/links" %> diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f526dc82..bad870f4 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -3,9 +3,9 @@ %html %head - title = yield :title - %title #{title.blank? ? "" : "#{title} | "}Hackety Hack! + %title=(title.blank? ? "" : title.gsub("\n", "")) + " | Hackety Hack!" - + / = stylesheet_link_tag "application" @@ -23,6 +23,12 @@ (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ga); })(); + + + + + = yield(:head) + %body{:class => "body-#{@page_class}"} %header.topbar .topbar-inner @@ -45,8 +51,15 @@ .alert-message.alert #{flash[:alert]} + = yield :alert + - unless title.empty? - %h1.title= title + .page-title + %h1.title + = title + %small= yield :subtitle + .feed + = yield :feed #content{:class => @content_class} = yield diff --git a/app/views/lessons/index.html.haml b/app/views/lessons/index.html.haml new file mode 100644 index 00000000..bb1efa39 --- /dev/null +++ b/app/views/lessons/index.html.haml @@ -0,0 +1,20 @@ +- content_for :title do + Lessons + +- content_for :sidebar do + = render :partial => "shared/ask" + = render :partial => "shared/lessons" + +%p Want to learn programming? Here are some lessons that we've put online. + +%ul#lessons + - @lessons.each do |lesson| + %li + .info + .title + = link_to lesson.metadata["title"], lesson_path(lesson.metadata["slug"]) + = lesson.metadata["blurb"] + .categories + = lesson_categories lesson + + diff --git a/app/views/lessons/show.html.haml b/app/views/lessons/show.html.haml new file mode 100644 index 00000000..27f26e1e --- /dev/null +++ b/app/views/lessons/show.html.haml @@ -0,0 +1,13 @@ +- @page_class = "lesson" + +- content_for :title do + = @lesson.metadata["title"] + +- content_for :sidebar do + = render :partial => "shared/ask" + = render :partial => "shared/lessons" + +#lesson-content + = lesson_categories @lesson + + =markdown(@lesson.output) diff --git a/app/views/mailer/_form.html.haml b/app/views/mailer/_form.html.haml new file mode 100644 index 00000000..52e95c84 --- /dev/null +++ b/app/views/mailer/_form.html.haml @@ -0,0 +1,11 @@ +=simple_form_for(@message, :url => mailer_path) do |f| + =f.error_notification + .inputs + =f.input :subject, :hint => "Write the subject here!" + =f.input :body, :as => :text + + -@emails.each do |email| + =f.input "email[#{email}]", :as => :text, :as => :hidden, :input_html => { :value => ""} + + .actions + =f.button :submit , 'Send Email', :class => "primary btn" \ No newline at end of file diff --git a/app/views/mailer/new.html.haml b/app/views/mailer/new.html.haml new file mode 100644 index 00000000..c9f04e11 --- /dev/null +++ b/app/views/mailer/new.html.haml @@ -0,0 +1,7 @@ +%h1 Send an Email! += render "form" + +This email will send to: +-@emails.each do |email| + = email + = " " \ No newline at end of file diff --git a/app/views/message_mailer/new_message.html.haml b/app/views/message_mailer/new_message.html.haml new file mode 100644 index 00000000..df3f577a --- /dev/null +++ b/app/views/message_mailer/new_message.html.haml @@ -0,0 +1 @@ +=@message.body \ No newline at end of file diff --git a/app/views/notification/new_answer.html.haml b/app/views/notification/new_answer.html.haml new file mode 100644 index 00000000..0c38fe81 --- /dev/null +++ b/app/views/notification/new_answer.html.haml @@ -0,0 +1,3 @@ +Hey there! You asked a question on Hackety.com: "" + +Someone has replied with an answer! Check it out here: #{@url} diff --git a/app/views/programs/index.html.haml b/app/views/programs/index.html.haml index 51934198..46f0a890 100644 --- a/app/views/programs/index.html.haml +++ b/app/views/programs/index.html.haml @@ -1,29 +1,34 @@ -%h1 Every Awesome Program We've Got +- content_for :title do + - if @user + #{@user.username}'s Programs + - else + Programs -Here's the motherload! Check out these great programs everybody has made! +- unless @featured.blank? or @user + #featured.programs + .heading + %h3 Featured + .description Our favorite selection of projects. -%h2 The Super Sweet List + %ul + - @featured.each do |program| + %li + .title= program_link program + .author= author_link program.author_username, program -We've picked out a few extra-awesome ones for your enjoyment. +#all-programs.programs + - unless @user + .heading + %h3 All Programs + .description Every program uploaded by our users! -%ul - %li - :markdown - [Connect Four](http://hackety-hack.com/users/wilkie/programs/connect_four), by [wilkie](http://hackety-hack.com/users/wilkie) - %li - :markdown - [Calender basic](http://hackety-hack.com/users/cyber/programs/calender_basic), by [cyber](http://hackety-hack.com/users/cyber) - %li - :markdown - [Space Invaders](http://hackety-hack.com/users/wilkie/programs/invaders), by [wilkie](http://hackety-hack.com/users/wilkie) - %li - :markdown - [Turtle Fun](http://hackety-hack.com/users/frog/programs/turtle_fun), by [frog](http://hackety-hack.com/users/frog) + %ul + - @programs.each do |program| + %li + .title= program_link program + .author= author_link program.author_username, program -%h2 Other Great Programs - -Here it is: every single program we've got. Check them out! - -%ul - - collection.each do |program| - %li #{link_to program.title, program_path(program)}, by #{link_to program.author_username, "/users/#{program.author_username}"} +- if @user + %ul.inline-menu.bottom-nav + %li= link_to "All Programs", programs_path + %li= link_to "#{@user.username}'s profile", user_path(@user) diff --git a/app/views/programs/show.html.haml b/app/views/programs/show.html.haml index 3dc2d1c7..b5ff2b1b 100644 --- a/app/views/programs/show.html.haml +++ b/app/views/programs/show.html.haml @@ -1,11 +1,30 @@ -%h1 '#{@program.title}', a program by #{@program.author_username} +- unless current_user + - content_for :alert do + .alert-message.block-message.info + %p + = author_link @program.author_username + and many others are using + %strong Hackety Hack + to learn how to code. + %p Hackety Hack is a free program for Windows, Mac OS X and Linux that can teach you how to make games, applications and more and share those programs with your friends. You can ask other budding programmers questions and follow them to see what code they're working on through the Hackety Hack website. -%pre - %code - = @program.source_code + = link_to "Sign up", new_user_registration_path, :class => "btn primary" + = link_to "Learn More", root_path, :class => "btn" -%hr/ +- content_for :title do + = @program.title -%h3 Other Programs +- content_for :subtitle do + %small + by + = link_to @program.author_username, user_path(@program.author_username) -Make sure to check out all the other programs other users have made on the #{link_to "All Programs page", programs_path}! +.description + %p= @program.description + +#program + %pre.prettyprint= @program.source_code + + %ul.inline-menu.bottom-nav + %li= link_to "#{@program.author_username}'s programs", user_programs_path(@program.author_username) + %li= link_to "All Programs", programs_path diff --git a/app/views/questions/_form.html.haml b/app/views/questions/_form.html.haml index ced38b9f..de4eec3f 100644 --- a/app/views/questions/_form.html.haml +++ b/app/views/questions/_form.html.haml @@ -1,10 +1,12 @@ -- curr_module = @support ? '/support' : '' -= simple_form_for(resource, :url => curr_module.concat(@form_url)) do |f| += simple_form_for(resource, :url => @form_url) do |f| = f.error_notification .inputs = f.input :title, :hint => "What's your question?" - = f.input :description, :as => :text, :hint => "Provide some details about your problem to help diagnose it" + - if @support + = f.hidden_field :support, :value => true + = f.input :description, :as => :text, + :hint => "Provide some details about your problem to help diagnose it You can format your input with Markdown".html_safe .actions - = f.button :submit , 'Ask Everyone', :class => "primary btn" \ No newline at end of file + = f.button :submit , 'Ask Everyone', :class => "primary btn" diff --git a/app/views/questions/_list.html.haml b/app/views/questions/_list.html.haml index fdea6bb2..fe52303d 100644 --- a/app/views/questions/_list.html.haml +++ b/app/views/questions/_list.html.haml @@ -1,13 +1,15 @@ -%li.item{:id=>list.id, :class => list.answers.count > 0 ? (list.solution_id.nil? ? "answered" : "accepted") : "unanswered"} +%li.item{:id=>question.id, :class => question.answers.count > 0 ? (question.solution_id.nil? ? "answered" : "accepted") : "unanswered"} .answer-count - %h3= list.answers.count - #{list.answers.count == 1 ? "answer" : "answers"} + %h3= question.answers.count + #{question.answers.count == 1 ? "answer" : "answers"} .summary - %h2.title= link_to list.title, resource_path(list) + %h2.title= link_to question.title, resource_path(question) .meta Asked by - %span.name= link_to list.user.username, user_path(list.user) + %span.name= link_to question.user.username, user_path(question.user) - %span.date= time_ago_in_words(list.created_at) + %span.date= time_ago_in_words(question.created_at) ago - .description= truncate(list.description, :length => 150) + .description= truncate(question.description, :length => 150) + - if current_user && current_user.moderator? + %div.move_question= link_to "Move to #{question.support? ? 'Questions' : 'Support'} »", question_path(question, :question => { :support => (question.support? ? "false" : "true") }), :method => :put, :class => "btn small info" diff --git a/app/views/questions/index.atom.builder b/app/views/questions/index.atom.builder new file mode 100644 index 00000000..09f14748 --- /dev/null +++ b/app/views/questions/index.atom.builder @@ -0,0 +1,19 @@ +atom_feed :language => 'en-US' do |feed| + feed.title "Hackety Hack Questions" + feed.updated @updated + + @questions.each do |question| + feed.entry( question ) do |entry| + entry.url question_url(question) + entry.title question.title + entry.content question.description + + # the strftime is needed to work with Google Reader. + entry.updated(question.updated_at.strftime("%Y-%m-%dT%H:%M:%SZ")) + + entry.author do |author| + author.name question.user.username + end + end + end +end \ No newline at end of file diff --git a/app/views/questions/index.html.haml b/app/views/questions/index.html.haml index b552bace..41739204 100644 --- a/app/views/questions/index.html.haml +++ b/app/views/questions/index.html.haml @@ -1,3 +1,9 @@ +- content_for :head do + - if @support + = auto_discovery_link_tag :atom, support_questions_url(format: :atom) + - else + = auto_discovery_link_tag :atom, questions_url(format: :atom) + - content_for :title do -if @support Support Questions @@ -11,7 +17,13 @@ - else = render :partial => "shared/support_blurb" +- content_for :feed do + - if @support + = link_to "Feed", support_questions_url(format: :atom) + - else + = link_to "Feed", questions_url(format: :atom) + %ul.questions - = render :partial => "list", :collection => collection + = render :partial => "list", :collection => collection, :as => :question -= will_paginate += will_paginate(@questions, renderer: @support ? PrefixLinkRenderer.new('/support') : nil) diff --git a/app/views/questions/new.html.haml b/app/views/questions/new.html.haml index 12ed3887..fc5dcc8a 100644 --- a/app/views/questions/new.html.haml +++ b/app/views/questions/new.html.haml @@ -1,4 +1,4 @@ - content_for :title do New Question -= render 'form' \ No newline at end of file += render 'form' diff --git a/app/views/questions/show.html.haml b/app/views/questions/show.html.haml index 10e39d98..b53a34b1 100644 --- a/app/views/questions/show.html.haml +++ b/app/views/questions/show.html.haml @@ -26,12 +26,12 @@ .description - :markdown - #{resource.description} + =markdown(resource.description) %h2= pluralize(resource.answers.count, "response") %ul.answers = render :partial => "answers/list", :collection => resource.answers - if can? :create, Answer - = render :partial => "answers/form" + = render :partial => "answers/short_answer" + diff --git a/app/views/shared/_ask.html.haml b/app/views/shared/_ask.html.haml index 0fd3ec35..7f7efda5 100644 --- a/app/views/shared/_ask.html.haml +++ b/app/views/shared/_ask.html.haml @@ -2,7 +2,10 @@ %h2 Have A Question? %p Ask away! No question is too big or too small. - if current_user - = link_to "Ask a Question", new_question_path, :class => "btn success" + - if @support + = link_to "Ask a Question", new_question_path(:support => true), :class => "btn success" + - else + = link_to "Ask a Question", new_question_path, :class => "btn success" - else %p Log in to ask a question - = link_to "Log In", login_path, :class => "btn success" \ No newline at end of file + = link_to "Log In", login_path, :class => "btn success" diff --git a/app/views/shared/_lessons.html.haml b/app/views/shared/_lessons.html.haml new file mode 100644 index 00000000..93419a2d --- /dev/null +++ b/app/views/shared/_lessons.html.haml @@ -0,0 +1,4 @@ +%section.lessons + %h2 Want to help? + %p Help us create new lessons, or update existing ones. Check out the Lessons project on Github below: + = link_to "Hackety Hack Lessons on Github", "https://github.com/hacketyhack/hackety_hack-lessons" \ No newline at end of file diff --git a/app/views/shared/_recent.html.haml b/app/views/shared/_recent.html.haml new file mode 100644 index 00000000..9a7460aa --- /dev/null +++ b/app/views/shared/_recent.html.haml @@ -0,0 +1,6 @@ +%section.recent + %h2 Recent Posts + %ul + - BlogPost.sort("created_at DESC").limit(5).each do |post| + %li + = link_to post.title, blog_path(post) diff --git a/app/views/shared/menu/_footer.html.haml b/app/views/shared/menu/_footer.html.haml index dbaef778..2cd33abe 100644 --- a/app/views/shared/menu/_footer.html.haml +++ b/app/views/shared/menu/_footer.html.haml @@ -1,7 +1,8 @@ = semantic_menu :class => "pills" do |root| - root.add "Home", root_path + - root.add "Blog", blog_index_path - root.add "Questions", questions_path - root.add "Programs", programs_path - root.add "FAQ", faq_path - root.add "Support", support_questions_path - \ No newline at end of file + diff --git a/app/views/shared/menu/_main.html.haml b/app/views/shared/menu/_main.html.haml index 9d91c405..41130bc5 100644 --- a/app/views/shared/menu/_main.html.haml +++ b/app/views/shared/menu/_main.html.haml @@ -1,5 +1,8 @@ = semantic_menu :class => "nav" do |root| + - root.add "Blog", blog_index_path - root.add "Questions", questions_path + - root.add "Lessons", lessons_path - root.add "Programs", programs_path - root.add "FAQ", faq_path - root.add "Support", support_questions_path + - root.add "Contribute", contribute_path diff --git a/app/views/shared/menu/_user.html.haml b/app/views/shared/menu/_user.html.haml index 3b507a9a..2e4de22e 100644 --- a/app/views/shared/menu/_user.html.haml +++ b/app/views/shared/menu/_user.html.haml @@ -1,5 +1,7 @@ = semantic_menu :class => "nav secondary-nav" do |root| - if current_user + - if current_user.moderator + - root.add "Send an Email", users_index_path - root.add current_user.username, user_path(current_user) - root.add "Log Out", logout_path - else diff --git a/app/views/static/contribute.html.haml b/app/views/static/contribute.html.haml new file mode 100644 index 00000000..bac61372 --- /dev/null +++ b/app/views/static/contribute.html.haml @@ -0,0 +1,26 @@ +- content_for :sidebar do + = render :partial => "shared/ask" + = render :partial => "shared/support_blurb" + +- content_for :title do + Contribute + +#Contribute + :markdown + Volunteers build Hackty Hack - want to help and join us to improve Hackety Hack? + + ## Want to help developing Hackety Hack? + + Both this web application and the Hackety Hack program itself are open source projects. This means volunteers build them, but it also means that you can jump in and help out! How cool would that be, right? + + You can help out with the development of this web application [here](https://github.com/hacketyhack/hackety-hack.com) and with the the development of the Hackety Hack program [here](https://github.com/hacketyhack/hacketyhack). + + Every contribution is very welcome! It can be a fixed typo, an issue report or an entirely new feature - that is up to you! Thanks in advance! + + ## Want to help us translate Hackety Hack? + + Hello everyone! + + We are in the process of translating Hackety Hack into as many foreign languages as possible so that people around the world can use the site with ease. + + If you are bilingual and interested in helping us make Hackety Hack a truly global phenomenon, accessible by all regardless of location or nationality and make learning Ruby even more fun, then please [sign up here!](http://crowdin.net/project/hackety-hackcom/invite) diff --git a/app/views/static/faq.html.haml b/app/views/static/faq.html.haml index c71f9439..8c2bbcb7 100644 --- a/app/views/static/faq.html.haml +++ b/app/views/static/faq.html.haml @@ -7,8 +7,6 @@ #faq :markdown - (or FAQ re: HH) - ##My install of Hackety-Hack crashes? Yes, we noticed that. Sorry. Multi-platform weirdness and all that. There are some known problems with HH on Windows Vista and XP, and they are being worked on. @@ -82,6 +80,6 @@ [Raindropmemory](http://raindropmemory.deviantart.com/#/d1xzrog) on Deviant Art made them. They're super great, aren't they? - # Thanks! + ## Thanks! - Special thanks goes out to Eric Affleck for writing this FAQ. + Special thanks goes out to Eric Affleck for writing this FAQ. \ No newline at end of file diff --git a/app/views/static/root.html.haml b/app/views/static/root.html.haml index 3227926e..6271f045 100644 --- a/app/views/static/root.html.haml +++ b/app/views/static/root.html.haml @@ -29,5 +29,5 @@ %section#ask %h1 Help is Here .content - %p Got a question about Hackety Hack? Check out our #{link_to "FAQ", "faq"}. If that doesn't cover it ask for support here + %p Got a question about Hackety Hack? Check out our #{link_to "FAQ", "faq"}. If that doesn't cover it ask for support #{link_to "here", support_questions_path}. %p Got a question about programming in general? Ask away in the #{link_to "Questions", questions_path} section. No question to big or small, the community is here to help you out. diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb deleted file mode 100644 index eca09d5b..00000000 --- a/app/views/users/_form.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> - <%= f.error_notification %> - -
- <%= f.input :username, :autofocus => true %> - <%= f.input :email %> - <%= f.input :password %> - <%= f.input :password_confirmation %> - <%= f.input :about, :as => :text %> -
- -
- <%= f.button :submit, "Sign up", :class => "btn primary" %> -
-<% end %> diff --git a/app/views/users/_form.html.haml b/app/views/users/_form.html.haml new file mode 100644 index 00000000..49e9154b --- /dev/null +++ b/app/views/users/_form.html.haml @@ -0,0 +1,17 @@ +- submit_label ||= "Update" += simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| + = f.error_notification + .inputs + = f.input :username, :autofocus => true + = f.input :email + .inputs + - pwd_options = resource.new_record? ? {} : {:hint => "Leave it blank if you don't want to change it"} + = f.input :password, pwd_options.merge({:required => false}) + = f.input :password_confirmation, :required => false + .inputs + = f.input :about, :as => :text + - unless resource.new_record? + .inputs + = f.input :current_password, :required => true, :hint => "We need your password to update your account." + .actions + = f.button :submit, submit_label, :class => "btn primary" diff --git a/app/views/users/deleted_user.html.haml b/app/views/users/deleted_user.html.haml new file mode 100644 index 00000000..10721f64 --- /dev/null +++ b/app/views/users/deleted_user.html.haml @@ -0,0 +1 @@ +%h2 This user has been deleted, sorry \ No newline at end of file diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100644 index 00000000..2a365243 --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,8 @@ +=link_to "Create a diffusion", mailer_path(:user => User.all) +%ul#user-list + - @users.each do |user| + %li + .title= user.username + = link_to "Email him", mailer_path(:user => user), :class => "btn small info" + += will_paginate @users \ No newline at end of file diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 44eac61e..d4a79eb7 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,29 +1,55 @@ - content_for :title do - = "#{resource.username}'s page" + = "#{resource.username}'s Profile" -.about-user - %img{:src => gravatar_url(resource.email)} - - if current_user && current_user != resource - -if current_user.following?(resource) - = simple_form_for(resource, :url => resource_path(current_user) + '/unfollow', :method=>:post) do |f| - = f.hidden_field :followee, :value => resource.id - = f.button :submit, :value => "Unfollow", :class => "primary btn" - - else - = simple_form_for(resource, :url => resource_path(current_user) + '/follow', :method=>:post) do |f| - = f.hidden_field :followee, :value => resource.id - = f.button :submit, :value => "Follow", :class => "primary btn" +-content_for :sidebar do + %section.about-user + %img{:src => gravatar_url(resource.email)} + - if current_user && current_user != resource + -if current_user.following?(resource) + = simple_form_for(resource, :url => resource_path(current_user) + '/unfollow', :method=>:post) do |f| + = f.hidden_field :followee, :value => resource.id + = f.button :submit, :value => "Unfollow", :class => "primary btn" + - else + = simple_form_for(resource, :url => resource_path(current_user) + '/follow', :method=>:post) do |f| + = f.hidden_field :followee, :value => resource.id + = f.button :submit, :value => "Follow", :class => "primary btn" + + - unless resource.about.blank? + %p= resource.about + %p + = link_to pluralize(resource.programs.count, "Program"), user_programs_path(resource), :class => "user-programs" + %p + Following: + =link_to resource.following.count, resource_path(resource) + "/following", :class => 'user-following' + %p + Followers: + = link_to resource.followers.count, resource_path(resource) + "/followers", :class => 'user-followers' + %hr + - if can? :update, resource + = link_to "Change My Settings", edit_registration_path(resource), :class => "btn success" + + +- unless @user.questions.empty? + %section.user-questions + .heading + %h3= pluralize(@user.questions.count, "Question") + %ul + - @user.questions.each do |question| + %li + .title + = link_to question.title, question_path(question) + .meta + asked #{question.created_at.strftime("%b %e, %l:%M %p")} + +- unless @user.answers.empty? + %section.user-answers + .heading + %h3= pluralize(@user.answers.count, "Answer") + %ul + - @user.answers.each do |answer| + %li + .description + = answer.description.truncate(100) + .meta + = link_to("View Answer", answer.question) - %h2= "About #{resource.username}" - %h4= resource.about - %hr - %p - = link_to "Programs", user_programs_path(resource) - %p - Following: - =link_to resource.following.count, resource_path(resource) + "/following" - %p - Followers: - = link_to resource.followers.count, resource_path(resource) + "/followers" - %hr - - if can? :update, resource - = link_to "Change My Settings", edit_registration_path(resource), :class => "btn success" diff --git a/config/application.rb b/config/application.rb index bdfec545..d2382e0a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,7 +11,7 @@ module HacketyHackCom class Application < Rails::Application - config.autoload_paths += %W(#{config.root}/lib) + config.autoload_paths << "#{config.root}/lib" << "#{config.root}/presenters" config.encoding = "utf-8" diff --git a/config/config.yml.sample b/config/config.yml.sample new file mode 100644 index 00000000..2ef60a26 --- /dev/null +++ b/config/config.yml.sample @@ -0,0 +1,9 @@ +# encoding: utf-8 +development: + # Your secret key for verifying the integrity of signed cookies. + # If you change this key, all old signed cookies will become invalid! + # Make sure the secret is at least 30 characters and all random, + # no regular words or you'll be exposed to dictionary attacks. + # Used in config/initializers/secret_token.rb. + # You can generate a good value for this by running `rake secret`. + SECRET_TOKEN: \ No newline at end of file diff --git a/config/cucumber.yml b/config/cucumber.yml index 19b288df..67669be1 100644 --- a/config/cucumber.yml +++ b/config/cucumber.yml @@ -1,7 +1,7 @@ <% rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" -rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" -std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +rerun_opts = rerun.to_s.strip.empty? ? "--color --format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--color --format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" %> default: <%= std_opts %> features wip: --tags @wip:3 --wip features diff --git a/config/environments/development.rb b/config/environments/development.rb index 07b0d262..e1189b93 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -26,4 +26,5 @@ config.assets.compress = false config.action_mailer.default_url_options = { :host => 'localhost:3000' } + config.action_mailer.delivery_method = :letter_opener end diff --git a/config/environments/production.rb b/config/environments/production.rb index 20a5678f..9562087c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -57,7 +57,7 @@ :authentication => :plain, :user_name => ENV['SENDGRID_USERNAME'], :password => ENV['SENDGRID_PASSWORD'], - :domain => 'hackety-hack.com' + :domain => 'hackety.com' } ActionMailer::Base.delivery_method = :smtp diff --git a/config/initializers/cve_fix.rb b/config/initializers/cve_fix.rb new file mode 100644 index 00000000..f8788d01 --- /dev/null +++ b/config/initializers/cve_fix.rb @@ -0,0 +1,2 @@ +ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML) + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index dfb39d99..0b2e519b 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -73,7 +73,7 @@ # You can use this to let your user access some features of your application # without confirming the account, but blocking it after a certain period # (ie 2 days). - # config.confirm_within = 2.days + # config.allow_unconfirmed_access_for = 2.days # Defines which key will be used when confirming an account # config.confirmation_keys = [ :email ] @@ -82,16 +82,9 @@ # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks - # If true, a valid remember token can be re-used between multiple browsers. - # config.remember_across_browsers = true - # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false - # If true, uses the password salt as remember token. This should be turned - # to false if you are not using database authenticatable. - config.use_salt_as_remember_token = true - # Options to be passed to the created cookie. For instance, you can set # :secure => true in order to force SSL only cookies. # config.cookie_options = {} @@ -153,10 +146,6 @@ # Defines name of the authentication token params key # config.token_authentication_key = :auth_token - # If true, authentication through token does not store user in session and needs - # to be supplied on each request. Useful if you are using the token as API token. - # config.stateless_token = false - # ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 41d098a4..5e4aedfb 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -1,7 +1,20 @@ -# Be sure to restart your server when you modify this file. +if ENV["SECRET_TOKEN"].blank? + if Rails.env.production? + raise "You must set ENV[\"SECRET_TOKEN\"] in your app's config vars" + elsif Rails.env.test? + # Generate the key and test away + ENV["SECRET_TOKEN"] = HacketyHackCom::Application.config.secret_token = SecureRandom.hex(30) + else + config_file = File.expand_path(File.join(Rails.root, '/config/config.yml')) + unless File.exist? config_file + require 'fileutils' + FileUtils.cp config_file + '.sample', config_file + end + config = YAML.load_file(config_file) + # Generate the key, set it for the current environment, update the yaml file and move on + ENV["SECRET_TOKEN"] = config[Rails.env]['SECRET_TOKEN'] = SecureRandom.hex(30) + File.open(config_file, 'w') { |file| file.write(config.to_yaml) } + end +end -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -HacketyHackCom::Application.config.secret_token = '855ad4d9d95703179ef4280cd14ec205cb6a1fb2477c11bdf55d80390a8ecae82e4b60a4d9e572f9a83cd4d74cedf32a0592afdf36fc86aff0da766fa866aafd' +HacketyHackCom::Application.config.secret_token = ENV["SECRET_TOKEN"] diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index dc586340..7cfda7fd 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -3,7 +3,7 @@ # Components used by the form builder to generate a complete input. You can remove # any of them, change the order, or even add your own components to the stack. # config.components = [ :placeholder, :label_input, :hint, :error ] - config.components = [ :placeholder, :label_input, :hint ] + config.components = [ :placeholder, :label_input, :hint, :error ] # Default tag used on hints. # config.hint_tag = :span diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index a5cf3bf6..93016092 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -17,6 +17,7 @@ en: unauthenticated: 'You need to sign in or sign up before continuing.' unconfirmed: 'You have to confirm your account before continuing.' locked: 'Your account is locked.' + not_found_in_database: 'Invalid email or password.' invalid: 'Invalid email or password.' invalid_token: 'Invalid authentication token.' timeout: 'Your session expired, please sign in again to continue.' @@ -33,10 +34,9 @@ en: send_paranoid_instructions: 'If your e-mail exists on our database, you will receive an email with instructions about how to confirm your account in a few minutes.' confirmed: 'Your account was successfully confirmed. You are now signed in.' registrations: - signed_up: 'Welcome! You have signed up successfully.' - inactive_signed_up: 'You have signed up successfully. However, we could not sign you in because your account is %{reason}.' - updated: 'You updated your account successfully.' - destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' + signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' + signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' + signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' unlocks: send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' unlocked: 'Your account was successfully unlocked. You are now signed in.' diff --git a/config/routes.rb b/config/routes.rb index f9144702..e46d05dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,15 +1,29 @@ HacketyHackCom::Application.routes.draw do + get "users/index" + + match 'mailer' => 'mailer#new', :as => 'mailer', :via => :get + match 'mailer' => 'mailer#create', :as => 'mailer', :via => :post + get 'users/deleted_user', :to => 'users#deleted_user' + + resources :lessons, :only => [:index, :show] + resources :questions do resources :answers end + resources :blog, :controller => "blog" do + match "admin", :on => :collection, :controller => "blog", :action => "admin" + end + get "/downloads/latest/:platform", :to => "static#download", :as => 'downloads' get "/downloads/latest", :to => "static#download", :as => 'download' match "/download" => redirect("/downloads/latest") scope '/support', :as => 'support' do - resources :questions, :controller => 'questions' + resources :questions, :controller => 'questions' do + resources :answers + end end constraints(ApiConstraint) do @@ -23,7 +37,8 @@ resources :programs, :only => [:index, :show] - devise_for :users do + devise_for :users + devise_scope :user do get "login" => "devise/sessions#new", :as => "login" get "logout" => "devise/sessions#destroy", :as => "logout" end @@ -40,6 +55,7 @@ # match ':user_id/:slug', :to => "programs#show", :as => :program match 'faq' => 'static#faq' + match 'contribute' => 'static#contribute' root :to => "static#root" end diff --git a/db/seeds.rb b/db/seeds.rb index d68bf12d..8c03291a 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -73,3 +73,23 @@ question.save! questions[i] = question end + +program_code = " +Turtle.draw do + background maroon + pencolor honeydew + 450.times do + forward 100 + turnright 70 + end +end +" + +5.times do |i| + program = Program.new(:author_username => users[i].username, + :title => Faker::Lorem.sentence, + :description => Faker::Lorem.sentences.join(" "), + :source_code => program_code) + program.save! +end + diff --git a/features/answers.feature b/features/answers.feature index e7bd240d..d647cebe 100644 --- a/features/answers.feature +++ b/features/answers.feature @@ -1,11 +1,18 @@ Feature: CRUD actions for answers As a user of the site, I can perform standard CRUD actions on answers - + Scenario: Create an answer - Given a question exists + And a question exists And I answer that question Then I should be notified that my answer was submitted + And an email should be sent to the author And I should be able to see my answer - - Scenario: Accept an answer + And my answer should show on my profile page + + Scenario: Edit an answer + And a question exists + And I answer that question + When I edit that answer + Then I should see the updated answer + diff --git a/features/blog.feature b/features/blog.feature new file mode 100644 index 00000000..4c538602 --- /dev/null +++ b/features/blog.feature @@ -0,0 +1,22 @@ +Feature: Hackety Blog + + As an admin, I can post to the hackety blog. + + Scenario: Read the blog + Given a blog post exists + When I visit the blog + Then I should see the title of that post + And I should see the content of that post + + Scenario: Read a post + Given a blog post exists + When I visit a blog post + Then I should see the title of that post + And I should see the content of that post + + Scenario: Post to the blog + Given I'm logged in as someone who can post to the blog + When I visit the blog admin page + And I fill out the new blog form + And I press "Create Blog post" + Then I should see that my post has been created diff --git a/features/lessons.feature b/features/lessons.feature new file mode 100644 index 00000000..0f3b92ad --- /dev/null +++ b/features/lessons.feature @@ -0,0 +1,13 @@ +Feature: Hackety Lessons + + As a user of the site, I can visit lessons page + + Scenario: View the lessons + When I visit lessons path + Then I should see the list of lessons + + Scenario: View specific lesson + When I visit lessons path + When I click on lesson's title + Then I should see lesson content + diff --git a/features/moderator.feature b/features/moderator.feature new file mode 100644 index 00000000..2071f212 --- /dev/null +++ b/features/moderator.feature @@ -0,0 +1,15 @@ +Feature: Moderate questions + + As a moderator, I should be able to delete questions from the site. + + Background: + Given there is a question from someone else + + Scenario: Delete a question + Given I am a moderator + Then I can delete that question + + Scenario: Normal users cannot moderate + Given I am not a moderator + Then I cannot delete that question + diff --git a/features/programs.feature b/features/programs.feature new file mode 100644 index 00000000..f2ead8ec --- /dev/null +++ b/features/programs.feature @@ -0,0 +1,29 @@ +Feature: Access Programs + + As a user of the site, I want to access users' programs + + Scenario: View a featured program + Given there is a featured program + Then I should be able to view a highlighted program + + Scenario: View my programs + Given a logged in user + And I have uploaded a program + Then I should be able to view my programs + + Scenario: View another user's programs + Given a user has uploaded a program + Then I should be able to view their programs + + Scenario: View an individual program + Given a user has uploaded a program + And I should be able to view their programs + When I click the first program link + Then I should see the program source + + Scenario: View a program with a description + Given a user has uploaded a program + And they have given their program a description + And I should be able to view their programs + When I click the first program link + Then I should see the description of their program diff --git a/features/questions.feature b/features/questions.feature index c7c2fb85..ac233e90 100644 --- a/features/questions.feature +++ b/features/questions.feature @@ -6,6 +6,7 @@ Feature: CRUD actions for question When I create a new question Then I should be told the question was created And I should be able to see the new question + And I should be able to see the question on my profile page Scenario: Update a question Given I am a user that has created a question diff --git a/features/questions_pagination.feature b/features/questions_pagination.feature new file mode 100644 index 00000000..3b0f35d6 --- /dev/null +++ b/features/questions_pagination.feature @@ -0,0 +1,13 @@ +Feature: Pagination for different types of questions + + As a user of the site, I can view pages of different types of questions + + Scenario: View support questions + Given support questions exist + When I visit the support questions page + Then I should see a link to the next page of support questions + + Scenario: View non-support questions + Given questions exist + When I visit the questions page + Then I should see a link to the next page of questions diff --git a/features/signup.feature b/features/signup.feature new file mode 100644 index 00000000..442dd0bd --- /dev/null +++ b/features/signup.feature @@ -0,0 +1,11 @@ +Feature: Sign up for an account + + As a new Hackety user, I want to create a new account and log in with it + + Scenario: Create an account via the signup form + When I register a new account + Then I should be logged in with my new account + + Scenario: Try to sign up using existing user name + When I register a duplicate account + Then I should see validation errors diff --git a/features/statics.feature b/features/statics.feature new file mode 100644 index 00000000..37294c25 --- /dev/null +++ b/features/statics.feature @@ -0,0 +1,19 @@ +Feature: Detect user's platform and provide download link + + As a fresh Hackety user, I want to download software for my current OS + + Scenario: MacOS User + Given "Mac" user agent + Then I should be able to download "mac" software + + Scenario: Windows User + Given "Windows" user agent + Then I should be able to download "windows" software + + Scenario: Linux User + Given "Linux" user agent + Then I should be able to download "linux" software + + Scenario: Android User + Given "android" user agent + Then I should be able to download "android" software diff --git a/features/step_definitions/answer_steps.rb b/features/step_definitions/answer_steps.rb index 37351127..515e2099 100644 --- a/features/step_definitions/answer_steps.rb +++ b/features/step_definitions/answer_steps.rb @@ -17,3 +17,26 @@ page.should have_content("#{@user.username} says") page.should have_content("Have you tried turning it off and on again?") end + +Then /^my answer should show on my profile page$/ do + visit user_path(@user) + page.should have_content(@question.title) + page.should have_content("Have you tried turning it off and on again?") +end + +Then /^an email should be sent to the author$/ do + ActionMailer::Base.deliveries.should_not be_empty +end + +When /^I edit that answer$/ do + visit question_path(@question) + page.find('.answer > .links').click_link('Edit') + fill_in 'Answer', with: 'Edit: Did you try magic?' + click_button 'Post Answer' +end + +Then /^I should see the updated answer$/ do + page.should have_content("#{@user.username} says") + page.should have_content('Edit: Did you try magic?') +end + diff --git a/features/step_definitions/blog_steps.rb b/features/step_definitions/blog_steps.rb new file mode 100644 index 00000000..a200644e --- /dev/null +++ b/features/step_definitions/blog_steps.rb @@ -0,0 +1,47 @@ +Given /^a blog post exists$/ do + @post = Fabricate(:blog_post) +end + +When /^I visit the blog$/ do + visit "/blog" +end + +Then /^I should see the content of that post$/ do + page.should have_content(@post.content) +end + +Then /^I should see the title of that post$/ do + page.should have_content(@post.title) +end + +Given /^I'm logged in as someone who can post to the blog$/ do + step %{a logged in user} + @user.blog_poster = true + @user.save +end + +When /^I visit the blog admin page$/ do + visit "/blog/admin" +end + +When /^I fill out the new blog form$/ do + #this is silly + @post = OpenStruct.new + @post.title = "A title" + + fill_in :blog_post_title, :with => "A title" + fill_in :blog_post_content, :with => "Some content" +end + +When /^I press "([^"]*)"$/ do |name| + click_button name +end + +Then /^I should see that my post has been created$/ do + page.should have_content("Post created!") +end + +When /^I visit a blog post$/ do + visit blog_path(@post) +end + diff --git a/features/step_definitions/lesson_steps.rb b/features/step_definitions/lesson_steps.rb new file mode 100644 index 00000000..d7355def --- /dev/null +++ b/features/step_definitions/lesson_steps.rb @@ -0,0 +1,17 @@ +When(/^I visit lessons path$/) do + visit lessons_path +end + +Then(/^I should see the list of lessons$/) do + page.should have_content("Lessons") + page.should have_content("A Tour of Hackety Hack") +end + +When(/^I click on lesson's title$/) do + click_link("A Tour of Hackety Hack") +end + +Then(/^I should see lesson content$/) do + expect(page).to have_title "A Tour of Hackety Hack" + page.should have_content("Welcome to the Hackety Hack tour!") +end diff --git a/features/step_definitions/program_steps.rb b/features/step_definitions/program_steps.rb new file mode 100644 index 00000000..619e275c --- /dev/null +++ b/features/step_definitions/program_steps.rb @@ -0,0 +1,71 @@ + Given /^there is a featured program$/ do + @program = Program.create!(:author_username => "username", + :slug => "slug", + :title => "My Featured Program", + :featured => true) +end + +Then /^I should be able to view a highlighted program$/ do + visit programs_path + within "#featured" do + page.should have_content("My Featured Program") + end +end + +def upload_program(user) + @program = Program.create!(:author_username => user.username, + :slug => "slug", + :title => "#{user.username}'s program", + :source_code => "puts 'Hello world'") +end + +Given /^I have uploaded a program$/ do + upload_program(@user) +end + +Given /^a user has uploaded a program$/ do + @user = User.create!(:username => "some_user", + :password => "password", + :password_confirmation => "password", + :email => "some_user@example.com") + upload_program(@user) +end + +Given /^they have given their program a description$/ do + @program = Program.last + @program.description = "A really cool program!" + @program.save +end + + +def visit_user_programs_page + visit user_path(@user) + within ".about-user" do + find(".user-programs").click() + end + page.should have_content(@program.title.titleize) +end + +Then /^I should be able to view my programs$/ do + visit_user_programs_page +end + +Then /^I should be able to view their programs$/ do + visit_user_programs_page +end + +When /^I click the first program link$/ do + within "#all-programs ul" do + first("li:first-child a").click() + end +end + +Then /^I should see the program source$/ do + within("#program") do + page.should have_content("puts 'Hello world'") + end +end + +Then /^I should see the description of their program$/ do + page.should have_content("A really cool program!") +end diff --git a/features/step_definitions/question_steps.rb b/features/step_definitions/question_steps.rb index 296afdc0..385d208e 100644 --- a/features/step_definitions/question_steps.rb +++ b/features/step_definitions/question_steps.rb @@ -43,3 +43,52 @@ def create_question_for(user) page.should have_content('An edited question') page.should have_content('Just a quick edit') end + +Then /^I should be able to see the question on my profile page$/ do + visit user_path(@user) + page.should have_content("My Question") +end + +Given /^I am a moderator$/ do + step %{a logged in user} + @user.moderator = true + @user.save +end + +Given /^I am not a moderator$/ do + step %{a logged in user} + @user.moderator = false + @user.save +end + +Given /^there is a question from someone else$/ do + @question = Question.create(:title => "A question to delete", + :description => "moderator should delete me!", + :user => Fabricate(:user)) +end + +Then /^I can delete that question$/ do + visit question_path(@question) + page.should have_content('Delete') +end + +Then /^I cannot delete that question$/ do + visit question_path(@question) + page.should_not have_content('Delete') +end + +Given /^(\w*)\s?questions exist$/ do |type| + 30.times do + Question.create(title: 'Test', description: 'Test', + support: type == 'support' ? true : false) + end +end + +When /^I visit the (\w*)\s?questions page$/ do |type| + visit(type == 'support' ? support_questions_path : questions_path) +end + +Then /^I should see a link to the next page of (\w*)\s?questions$/ do |type| + href = type == 'support' ? '/support/questions?page=2' : '/questions?page=2' + page.should have_link '2', href: href +end diff --git a/features/step_definitions/signup_steps.rb b/features/step_definitions/signup_steps.rb new file mode 100644 index 00000000..9d1cdb15 --- /dev/null +++ b/features/step_definitions/signup_steps.rb @@ -0,0 +1,31 @@ +def register_user(user) + visit new_user_registration_path + fill_in("Username", :with => user[:username]) + fill_in("Email", :with => user[:email]) + fill_in("Password", :with => user[:password]) + fill_in("Password confirmation", :with => user[:password]) + + click_button "Sign up" +end + +When /^I register a new account$/ do + @new_user = {:username => "username", :password => "password", :email => "test@example.com"} + register_user @new_user +end + +When /^I register a duplicate account$/ do + @existing_user = User.create!(username: "existing_user", + email: "existing_user@example.com", + password: "foobar", + password_confirmation: "foobar") + register_user @existing_user +end + +When /^I should be logged in with my new account$/ do + page.should have_content("You have signed up successfully") + page.should have_content(@new_user[:username]) +end + +Then /^I should see validation errors$/ do + page.should have_selector(".error_notification") +end diff --git a/features/step_definitions/static_steps.rb b/features/step_definitions/static_steps.rb new file mode 100644 index 00000000..b09a519a --- /dev/null +++ b/features/step_definitions/static_steps.rb @@ -0,0 +1,8 @@ +Given(/^"(.*?)" user agent$/) do |agent| + page.driver.header('User-Agent', agent) +end + +Then(/^I should be able to download "(.*?)" software$/) do |system| + visit download_path + page.should have_content("Version 1.0.1 for #{system}") +end \ No newline at end of file diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb index dd5c3554..dafb78b1 100644 --- a/features/step_definitions/user_steps.rb +++ b/features/step_definitions/user_steps.rb @@ -1,25 +1,39 @@ def login_user - @user = User.create!(username: "test_user", - email: "test_user@example.com", - password: "foobar", - password_confirmation: "foobar") - visit('/login') + @user = User.create!(username: "test_user", + email: "test_user@example.com", + password: "foobar", + password_confirmation: "foobar") + visit login_path fill_in("Username", :with => @user.username) fill_in("Password", :with => @user.password) click_button("Sign in") end +def create_other_user + @other_user = User.create!(username: "other_user", + email: "other_user@example.com", + password: "123456", + password_confirmation: "123456") +end + Given /^a logged in user$/ do login_user unless @user end +Given /^a steve exists$/ do + @steve = User.create!(username: "steve", + email: "steve_user@example.com", + password: "foobar", + password_confirmation: "foobar") +end + When /^I go to look at my profile page$/ do visit user_path(@user) end Then /^it should have the right information$/ do - page.should have_selector('title', :content => "#{@user.username}'s page") - page.should have_content("About #{@user.username}") + page.should have_title("#{@user.username}'s Profile\n | Hackety Hack!") + page.should have_content("#{@user.username}'s Profile") end When /^I edit my profile$/ do @@ -29,6 +43,34 @@ def login_user click_button "Update" end +When /^I have a follower$/ do + create_other_user + @other_user.follow! @user +end + +When /^I am following someone$/ do + create_other_user + @user.follow! @other_user +end + +When /^I click on the number of followers on my profile$/ do + step 'I go to look at my profile page' + first('.user-followers').click +end + +When /^I click on the number of people I am following on my profile$/ do + step 'I go to look at my profile page' + first('.user-following').click +end + +Then /^I should see someone I'm following$/ do + page.should have_link @other_user.username +end + +Then /^I should see my follower$/ do + page.should have_link @other_user.username +end + Then /^I should be notified that my profile was updated$/ do page.should have_content("updated your account") end @@ -37,3 +79,8 @@ def login_user visit("/users/#{@user.username}") page.should have_content("Test user likes to edit his profile") end + +Then(/^I should see 'Steve'$/) do + @user.following?(@steve).should == true + page.should have_link @steve.username +end diff --git a/features/support/env.rb b/features/support/env.rb index 07737733..25e027c5 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -3,8 +3,12 @@ # newer version of cucumber-rails. Consider adding your own code to a new file # instead of editing this one. Cucumber will automatically load all features/**/*.rb # files. +# + +require_relative '../../spec/code_coverage' require 'cucumber/rails' +require 'ruby-debug' # Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In # order to ease the transition to Capybara we set the default here. If you'd diff --git a/features/support/mailer.rb b/features/support/mailer.rb new file mode 100644 index 00000000..8cdfb2f1 --- /dev/null +++ b/features/support/mailer.rb @@ -0,0 +1,3 @@ +Before do + ActionMailer::Base.deliveries.clear +end diff --git a/features/users.feature b/features/users.feature index 6cb18e17..2d67ae5d 100644 --- a/features/users.feature +++ b/features/users.feature @@ -1,15 +1,30 @@ Feature: Manage account - - As a user of this site, I can view and update my profile - Background: The user is logged in - Given a logged in user + As a user of this site, I can view and update my profile, see my followers + + Background: + Given a steve exists + And a logged in user Scenario: View my profile When I go to look at my profile page Then it should have the right information - + Scenario: Edit my profile When I edit my profile Then I should be notified that my profile was updated And I should see my changes reflected on my profile page + + Scenario: See my followers + When I have a follower + And I click on the number of followers on my profile + Then I should see my follower + + Scenario: See who I am following + When I am following someone + And I click on the number of people I am following on my profile + Then I should see someone I'm following + + Scenario: I should always follow steve and vice versa + When I click on the number of people I am following on my profile + Then I should see 'Steve' diff --git a/lib/prefix_link_renderer.rb b/lib/prefix_link_renderer.rb new file mode 100644 index 00000000..79efb238 --- /dev/null +++ b/lib/prefix_link_renderer.rb @@ -0,0 +1,12 @@ +class PrefixLinkRenderer < WillPaginate::ActionView::LinkRenderer + def initialize(prefix) + @prefix = prefix + super() + end + + protected + + def url(page) + @prefix + super + end +end diff --git a/public/404.html b/public/404.html index 843e22d8..a663cf70 100644 --- a/public/404.html +++ b/public/404.html @@ -1,48 +1,44 @@ - - The page you were looking for doesn't exist (404) - - - - -
- - Questions -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address, the page may have moved or been deleted.

+ + The page you were looking for doesn't exist (404) + + + + + + + + + + +
+ +
+
+
+ +
+
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address, the page may have moved or been deleted.

+
+
+
- - + + diff --git a/public/422.html b/public/422.html index 83660ab1..197302a9 100644 --- a/public/422.html +++ b/public/422.html @@ -1,26 +1,44 @@ - - The change you wanted was rejected (422) - - - - - -
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
- + + The change you wanted was rejected (422) + + + + + + + + + + +
+ +
+
+
+ +
+
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+
+
+
+ + diff --git a/public/500.html b/public/500.html index 29660ecb..4ed09672 100644 --- a/public/500.html +++ b/public/500.html @@ -1,48 +1,44 @@ - - We're sorry, but something went wrong (500) - - - - -
- - Questions -
-
-

We're sorry, but something went wrong.

-

We've been notified about this issue and we'll take a look at it shortly.

+ + We're sorry, but something went wrong (500) + + + + + + + + + + +
+ +
+
+
+ +
+
+
+

We're sorry, but something went wrong.

+

We've been notified about this issue and we'll take a look at it shortly.

+
+
+
- - + + diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 00000000..de77ea4d --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,58 @@ +#!/bin/bash + +go_go_gadget_bootstrap() { + check_ruby + check_mongo + check_bundler + bail_unless_chill + good_luck_have_fun +} + +check_mongo() { + which mongo > /dev/null 2>&1 + if [ $? -ne 0 ]; then + NOMONGO="Hey, in order to remember stuff I need MongoDB" + fi +} + +check_bundler() { + if [ -z "$NORUBY" ]; then + which bundle > /dev/null 2>&1 + if [ $? -ne 0 ]; then + gem install bundler || exit 2 + fi + fi +} + +good_luck_have_fun() { + bundle install + rake spec cucumber +} + +check_ruby() { + which ruby > /dev/null 2>&1 + if [ $? -ne 0 ]; then + NORUBY="Whoa there, looks like you're missing Ruby!" + else + ruby -v | grep '^ruby 1.9.3' > /dev/null 2>&1 + if [ $? -ne 0 ]; then + NORUBY="Hrmm, your Ruby version isn't Ruby 1.9.3, that's really what I prefer." + fi + fi +} + +bail_unless_chill() { + if [ "$NORUBY" -a "$NOMONGO" ]; then + echo "$NORUBY" + echo "$NOMONGO" + exit 1 + elif [ "$NORUBY" ]; then + echo "$NORUBY" + exit 1 + elif [ "$NOMONGO" ]; then + echo "$NOMONGO" + exit 1 + fi +} + +go_go_gadget_bootstrap diff --git a/spec/code_coverage.rb b/spec/code_coverage.rb new file mode 100644 index 00000000..605db4e3 --- /dev/null +++ b/spec/code_coverage.rb @@ -0,0 +1,13 @@ +require 'simplecov' +require 'coveralls' + +SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ + Coveralls::SimpleCov::Formatter, + SimpleCov::Formatter::HTMLFormatter +] +SimpleCov.start do + add_filter '/config/' + add_filter '/vendor/' + add_filter '/spec/' + add_filter '/features/' +end \ No newline at end of file diff --git a/spec/controllers/blog_controller_spec.rb b/spec/controllers/blog_controller_spec.rb new file mode 100644 index 00000000..8da8d9df --- /dev/null +++ b/spec/controllers/blog_controller_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe BlogController do + let(:blog_post) { Fabricate(:blog_post) } + let(:another_blog_post) { Fabricate(:another_blog_post) } + + describe('#index') do + it "gets index successfully" do + get :index + response.should be_successful + end + + describe("handles posts by") do + before(:each) do + blog_post + another_blog_post + get :index + @posts = assigns(:posts) + end + it "fetching posts" do + @posts.size.should == 2 + end + it "sorting them in reverse order" do + @posts.first.content[0].should > @posts.last.content[0] + end + end + end + + describe "#show" do + before(:each) do + blog_post + get :show, id: blog_post + end + it { response.should be_successful } + it { assigns(:post).should == blog_post } + end + + describe "#admin" do + context "when user is not signed in" do + before(:each) do + get :create + end + #TODO Should it not use signin_path? + it { response.should redirect_to(new_user_session_path) } + end + + context "when user is not an blog_poster" do + before(:each) do + blog_post + get :admin + end + it { response.should be_redirect } + it { assigns(:post).should be_nil } + it { assigns(:posts).should be_nil } + end + context "when user is a blog_poster" do + before(:each) do + sign_in Fabricate(:user, blog_poster: true) + blog_post + get :admin + end + it { response.should be_successful } + it { assigns(:post).should be_a_new(BlogPost) } + it { assigns(:posts).size.should == 1 } + end + end + + describe "#create" do + context "when user is not signed in" do + before(:each) do + get :create + end + #TODO Should it not use signin_path? + it { response.should redirect_to(new_user_session_path) } + end + + context "when user is not an blog_poster" do + before(:each) do + sign_in Fabricate(:user) + get :create + end + + it { response.should redirect_to(blog_index_path) } + + it { assigns(:post).should be_nil } + end + + context "when user is a blog_poster" do + before(:each) do + sign_in Fabricate(:user, blog_poster: true) + end + + it "redirects to admin_blog_index_path when user is blog_poster" do + post :create + response.should redirect_to(admin_blog_index_path) + end + + it "creates a new blogpost" do + ->{ post :create, blog_post: Fabricate.build(:blog_post).attributes } + .should change(BlogPost, :count).by(1) + end + end + end +end diff --git a/spec/controllers/mailer_controller_spec.rb b/spec/controllers/mailer_controller_spec.rb new file mode 100644 index 00000000..2c5f7554 --- /dev/null +++ b/spec/controllers/mailer_controller_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +describe MailerController do + let(:user) { Fabricate(:user) } + let(:users) { Fabricate.sequence(:user, 5)} + let(:diffusion) { Fabricate.build(:diffusion) } + + shared_examples 'unauthorized' do + it 'redirects to the login page' do + response.should redirect_to(login_path) + end + end + + describe "GET 'new' for a single user email" do + + context 'when user is a moderator' do + before { sign_in Fabricate(:user, moderator: true) } + it "returns http success" do + get :new, user: Array(user) + response.should be_success + end + end + + context 'when user is not a moderator' do + before do + sign_in Fabricate(:user) + get :new, user: Array(user) + end + it_behaves_like 'unauthorized' + end + + context 'when user is a guest' do + before { get :new, user: Array(user) } + it_behaves_like 'unauthorized' + end + end + + describe "GET 'new' for a diffusion" do + context 'when user is a moderator' do + before { sign_in Fabricate(:user, moderator: true) } + it "returns http success" do + get :new, user: Array(users) + response.should be_success + end + end + + context 'when user is not a moderator' do + before do + sign_in Fabricate(:user) + get :new, user: Array(users) + end + it_behaves_like 'unauthorized' + end + + context 'when user is a guest' do + before { get :new, user: Array(users) } + it_behaves_like 'unauthorized' + end + end + + describe "POST 'create' for a single user email" do + before :each do + @message = Fabricate.build(:message) + end + + context 'when user is a moderator' do + before { sign_in Fabricate(:user, moderator: true) } + it 'delivers the email' do + expect { + post :create, message: @message + }.to change {ActionMailer::Base.deliveries.size}.by(1) + end + + describe 'delivered message' do + before :each do + post :create, message: @message + end + + it "returns http success" do + response.should be_redirect + end + + it 'delivers the mail with the subject that we wanted to' do + ActionMailer::Base.deliveries.last.subject.should == @message.subject + end + + it 'delivers the mail with the body that we wanted to' do + ActionMailer::Base.deliveries.last.body.to_s.should match @message.body + end + end + + describe 'empty message' do + it "returns exception" do + post :create + expect(subject).to render_template(:new) + end + end + end + + context 'when user is not a moderator' do + before do + sign_in Fabricate(:user) + post :create, message: @message + end + it_behaves_like 'unauthorized' + end + + context 'when user is a guest' do + before { post :create, message: @message } + it_behaves_like 'unauthorized' + end + end + + describe "POST 'create' for a diffusion" do + context 'when user is a moderator' do + before { sign_in Fabricate(:user, moderator: true) } + it "returns http success" do + expect { + post 'create', message: diffusion + }.to change {ActionMailer::Base.deliveries.size}.by(diffusion.email.size) + end + end + + context 'when user is not a moderator' do + before do + sign_in Fabricate(:user) + post :create, message: diffusion + end + it_behaves_like 'unauthorized' + end + + context 'when user is a guest' do + before { post :create, message: diffusion } + it_behaves_like 'unauthorized' + end + end +end diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb new file mode 100644 index 00000000..c6e15a89 --- /dev/null +++ b/spec/controllers/questions_controller_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe QuestionsController do + describe "GET index" do + context "with format atom" do + it "responds with success" do + get :index, format: :atom + response.should be_success + end + end + end +end diff --git a/spec/controllers/rel_controller_spec.rb b/spec/controllers/rel_controller_spec.rb new file mode 100644 index 00000000..ac13dba5 --- /dev/null +++ b/spec/controllers/rel_controller_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Api::RelsController do + let(:slug){Fabricate(:rel)} + + describe "GET index" do + it "responds with success" do + get :index + response.should be_success + end + end + + describe "GET show" do + it "responds with success" do + get :show + response.should be_success + end + end +end \ No newline at end of file diff --git a/spec/controllers/static_controller_spec.rb b/spec/controllers/static_controller_spec.rb new file mode 100644 index 00000000..ece397af --- /dev/null +++ b/spec/controllers/static_controller_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe StaticController do + describe "GET root" do + it "responds with success" do + expect(:get => "/").to route_to(:controller => "static", :action => "root") + end + end + + describe "GET api_root" do + it "should render api layout" do + get :api_root + response.should render_template(:layout => "api") + end + end + + describe "GET newest_version" do + it "should render version of api" do + get :newest_version + response.header['Content-Type'].should include 'application/json' + response.body.should include "1.0.0".to_json + end + end +end \ No newline at end of file diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb new file mode 100644 index 00000000..f756c742 --- /dev/null +++ b/spec/controllers/user_controller_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe UsersController do + let(:bob){Fabricate(:user)} + let(:mozart){Fabricate(:user)} + + describe '#index' do + context "When user is no moderator" do + it "try to get index" do + get :index + expect(response).to redirect_to(root_path) + end + end + + context "When user is moderator" do + before { sign_in Fabricate(:user, moderator: true) } + it "gets index with authorization" do + get :index + response.should be_success + end + end + end + + describe 'Following actions' do + before { sign_in bob } + it 'Try to follow himself' do + post :follow, user_id: bob, :user => {:followee => bob.id} + expect(flash[:notice]).to eq("You can't follow yourself silly!") + end + + it '#follow just once' do + post :follow, user_id: bob, :user => {:followee => mozart.id} + expect(flash[:notice]).to eq("You're following #{mozart.username} now") + post :follow, user_id: bob, :user => {:followee => mozart.id} + expect(flash[:notice]).to eq("You're already following #{mozart.username}") + end + + it '#following?' do + get :following, user_id: bob + response.should be_success + end + + it '#followers' do + get :followers, user_id: bob + response.should be_success + end + + it '#unfollow' do + post :unfollow, user_id: bob, :user => {:followee => mozart.id} + expect(flash[:notice]).to eq("You're no longer following #{mozart.username}") + end + end + + describe 'Deleted user' do + it 'should create new deleted user' do + get :deleted_user + response.should be_success + end + end +end diff --git a/spec/fabricators/answer_fabricator.rb b/spec/fabricators/answer_fabricator.rb index a02e7ab2..25f90dd5 100644 --- a/spec/fabricators/answer_fabricator.rb +++ b/spec/fabricators/answer_fabricator.rb @@ -1,4 +1,3 @@ Fabricator(:answer) do description "MyString" - user "" end \ No newline at end of file diff --git a/spec/fabricators/blog_post_fabricator.rb b/spec/fabricators/blog_post_fabricator.rb new file mode 100644 index 00000000..ff271066 --- /dev/null +++ b/spec/fabricators/blog_post_fabricator.rb @@ -0,0 +1,9 @@ +Fabricator(:blog_post) do + content "Hello, world" + title "Title" +end + +Fabricator(:another_blog_post, from: :blog_post) do + content "World, Hello" + title "Eltit" +end diff --git a/spec/fabricators/message_fabricator.rb b/spec/fabricators/message_fabricator.rb new file mode 100644 index 00000000..9ec6949e --- /dev/null +++ b/spec/fabricators/message_fabricator.rb @@ -0,0 +1,11 @@ +Fabricator(:message) do + email(count: 1) {"proof@example.com" } + subject:"Hackety-hack email" + body:"This is an email from hackety-hack.com" +end + +Fabricator(:diffusion, from: :message) do + email(count: 5) { |i| "proof#{i}@example.com" } + subject:"Hackety-hack email" + body:"This is an email from hackety-hack.com" +end diff --git a/spec/fabricators/question_fabricator.rb b/spec/fabricators/question_fabricator.rb index 2da52be4..74c4c5ea 100644 --- a/spec/fabricators/question_fabricator.rb +++ b/spec/fabricators/question_fabricator.rb @@ -1,4 +1,5 @@ Fabricator(:question) do + id "RandomID" title "Title" description "Description" user diff --git a/spec/following_policy_spec.rb b/spec/following_policy_spec.rb index 67cdf8bb..0a855bbc 100644 --- a/spec/following_policy_spec.rb +++ b/spec/following_policy_spec.rb @@ -1,28 +1,28 @@ require_relative '../app/models/following_policy' describe FollowingPolicy do - before(:each) do - @followee = stub() - @follower = stub() + before(:each) do + @followee = double 'followee' + @follower = double 'follower' end - + it 'can follow another' do @follower.should_receive(:following?).with(@followee).and_return(false) policy = FollowingPolicy.new(@follower) policy.can_follow?(@followee).should be_true end - + it "cannot follow self" do policy = FollowingPolicy.new(@follower) policy.following_self?(@follower).should be_true policy.can_follow?(@follower).should be_false end - + it "cannot follow twice" do @follower.should_receive(:following?).twice.with(@followee).and_return(true) policy = FollowingPolicy.new(@follower) policy.already_following?(@followee).should be_true policy.can_follow?(@followee).should be_false end - + end diff --git a/spec/helpers/program_helper_spec.rb b/spec/helpers/program_helper_spec.rb new file mode 100644 index 00000000..8a4e4c6e --- /dev/null +++ b/spec/helpers/program_helper_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe ProgramsHelper do + let(:simple) { Fabricate(:program) } + let(:complicated) { Fabricate(:program, :author_username => nil) } + + describe "#program_link" do + it "returns author's program path if author username exists" do + helper.program_link(simple).should == "#{link_to(simple.title.titleize, + user_program_path(simple.author_username, simple))}".html_safe + end + + it "returns url by program's slug if author username doesn't exist" do + helper.program_link(complicated).should == "#{link_to(complicated.title.titleize, + url_for(:controller => '/programs', + :action => 'show', + :id => complicated.slug))}".html_safe + end + end +end \ No newline at end of file diff --git a/spec/mailers/message_mailer_spec.rb b/spec/mailers/message_mailer_spec.rb new file mode 100644 index 00000000..b1a567fe --- /dev/null +++ b/spec/mailers/message_mailer_spec.rb @@ -0,0 +1,21 @@ +require "spec_helper" + +describe MessageMailer do + let(:message) { MessageMailer.new_message(Fabricate(:message), Array("proof@example.com"))} + + it "is from Steve" do + message.from.should eq(["steve@hackety.com"]) + end + + it "Correct subject" do + message.subject.should eq("Hackety-hack email") + end + + it "Correct receiver email" do + message.to.should eq(["proof@example.com"]) + end + + it "Correct body" do + message.body.encoded.should match("This is an email from hackety-hack.com") + end +end diff --git a/spec/mailers/notification_spec.rb b/spec/mailers/notification_spec.rb new file mode 100644 index 00000000..6fba9fb0 --- /dev/null +++ b/spec/mailers/notification_spec.rb @@ -0,0 +1,24 @@ +require "spec_helper" + +describe Notification do + describe "new_answer" do + let(:question) { Fabricate(:question) } + let(:mail) { Notification.new_answer(question) } + + it "has a link to the question" do + mail.body.encoded.should match(question_url(question, :host => "hackety.com")) + end + + it "is from Steve" do + mail.from.should eq(["steve@hackety.com"]) + end + + it "is to the question's author" do + mail.to.should eq([question.user.email]) + end + + it "has the proper subject" do + mail.subject.should eq("New Answer on Hackety.com!") + end + end +end diff --git a/spec/models/deleted_user_spec.rb b/spec/models/deleted_user_spec.rb new file mode 100644 index 00000000..9afc850b --- /dev/null +++ b/spec/models/deleted_user_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe DeletedUser do + + before do + @user = DeletedUser.new + end + + it 'has deleted username' do + @user.username.should == 'Deleted User' + end + + it 'has no email' do + @user.email.should == 'none' + end + + it 'returns deleted user param' do + @user.to_param.should == 'deleted_user' + end +end \ No newline at end of file diff --git a/spec/models/message.rb b/spec/models/message.rb new file mode 100644 index 00000000..5caa4eb1 --- /dev/null +++ b/spec/models/message.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Message do + subject {Fabricate.build(:message)} + + describe '#to_param' do + it 'has the email attribute' do + subject.to_param[:email].should == subject.email + end + + it'has the subject attribute' do + subject.to_param[:subject].should == subject.subject + end + + it 'has the body attribute' do + subject.to_param[:body].should == subject.body + end + end +end \ No newline at end of file diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb new file mode 100644 index 00000000..131fdeab --- /dev/null +++ b/spec/models/question_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Question do + + let(:question){Fabricate(:question)} + + it 'can be created validly' do + question.should be_valid + end + + describe 'without a user' do + before :each do + question.user.destroy + @reloaded_question = Question.find question.id + end + + it 'does not respond with nil when asked for its user' do + @reloaded_question.user.should_not be_nil + end + + it 'responds with something when ask for user that respodns to username' do + @reloaded_question.user.should respond_to :username + end + end +end \ No newline at end of file diff --git a/spec/models/rel_spec.rb b/spec/models/rel_spec.rb new file mode 100644 index 00000000..a614be8e --- /dev/null +++ b/spec/models/rel_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Rel do + + before do + @rel = Rel.create(:slug => 'text', :description => 'some text') + end + + context 'to_param' do + it 'returns slug' do + @rel.to_param.should == 'text' + end + end +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 00000000..c8d70a56 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe User do + let(:bob){Fabricate(:user, :username => 'hacker')} + let(:mozart){Fabricate(:user)} + let(:hello_world){Fabricate(:program, :author_username => bob.username)} + + it 'can be created validly' do + bob.should be_valid + mozart.should be_valid + end + + it 'Users can follow and unfollow' do + bob.follow!(mozart) + bob.following?(mozart).should be_true + bob.unfollow!(mozart) + bob.reload + bob.following?(mozart).should be_false + end + + describe 'programs' do + it 'should return programs list' do + bob.programs.class.should == Plucky::Query + hello_world.author_username.should == 'hacker' + bob.username.should == 'hacker' + bob.programs.count.should == 1 + bob.programs.first.should == hello_world + end + end +end \ No newline at end of file diff --git a/spec/presenters/question_presenter_spec.rb b/spec/presenters/question_presenter_spec.rb new file mode 100644 index 00000000..13463fc7 --- /dev/null +++ b/spec/presenters/question_presenter_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe QuestionPresenter do + + let(:question){Fabricate(:question)} + let(:another_question){Fabricate(:question, :id => 'AnotherID')} + let(:answer){Fabricate(:answer, :question_id => question.id)} + + before do + @qp = QuestionPresenter.new(question) + end + + context 'collection_path' do + it 'returns questions_path' do + @qp.collection_path.should == '/questions' + end + end + + context 'edit_resource_path' do + it 'returns edit_question_path' do + @qp.edit_resource_path.should == '/questions/RandomID/edit' + end + end + + context 'new_resource_path' do + it 'returns new_question_path' do + @qp.new_resource_path.should == '/questions/new' + end + end + + context 'resource_path' do + it 'returns self question path if nil' do + @qp.resource_path(nil).should == '/questions/RandomID' + end + + it 'returns question path if not nil' do + @qp.resource_path(another_question).should == '/questions/AnotherID' + end + end + + context 'answers_path' do + it 'returns questions answer path' do + @qp.answers_path.should == '/questions/RandomID/answers' + end + end +end \ No newline at end of file diff --git a/spec/presenters/support_presenter_spec.rb b/spec/presenters/support_presenter_spec.rb new file mode 100644 index 00000000..6dd9ac83 --- /dev/null +++ b/spec/presenters/support_presenter_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe SupportPresenter do + + let(:question){Fabricate(:question)} + let(:another_question){Fabricate(:question, :id => 'AnotherID')} + + before do + @sp = SupportPresenter.new(question) + end + + context 'page_title' do + it 'returns support questions title' do + @sp.page_title.should == 'Support Questions' + end + end + + context 'collection_path' do + it 'returns support questions_path' do + @sp.collection_path.should == '/support/questions' + end + end + + context 'edit_resource_path' do + it 'returns edit_support_question_path' do + @sp.edit_resource_path.should == '/support/questions/RandomID/edit' + end + end + + context 'new_resource_path' do + it 'returns new_support_question_path' do + @sp.new_resource_path.should == '/support/questions/new' + end + end + + context 'resource_path' do + it 'returns self question path if nil' do + @sp.resource_path(nil).should == '/support/questions/RandomID' + end + + it 'returns question path if not nil' do + @sp.resource_path(another_question).should == '/support/questions/AnotherID' + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5b980bc2..d80bdb02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,10 @@ +require 'code_coverage' + ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'database_cleaner' +require 'capybara/rspec' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -23,4 +26,10 @@ config.before(:each) do DatabaseCleaner[:mongo_mapper].clean end + + config.include(MailerMacros) + config.before(:each) { reset_email } + + config.include Devise::TestHelpers, :type => :controller + config.include Capybara::DSL end diff --git a/spec/support/mailer_macros.rb b/spec/support/mailer_macros.rb new file mode 100644 index 00000000..f082fda9 --- /dev/null +++ b/spec/support/mailer_macros.rb @@ -0,0 +1,10 @@ +module MailerMacros + def last_email + ActionMailer::Base.deliveries.last + end + + def reset_email + ActionMailer::Base.deliveries = [] + end +end + diff --git a/spec/unit/program_spec.rb b/spec/unit/program_spec.rb index 172d9bd7..c93ba99e 100644 --- a/spec/unit/program_spec.rb +++ b/spec/unit/program_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Program do - let(:prog) { Program.create(author_username: 'tester', title: 'test title')} + let(:prog) { Program.create(author_username: 'tester', title: 'test title')} + it "sets the slug" do prog.slug.should == 'test-title' end diff --git a/spec/unit/sluggifier_spec.rb b/spec/unit/sluggifier_spec.rb index 19afa177..9cc71ce9 100644 --- a/spec/unit/sluggifier_spec.rb +++ b/spec/unit/sluggifier_spec.rb @@ -22,6 +22,4 @@ end end end - - end diff --git a/spec/views/programs_index.rb b/spec/views/programs_index.rb new file mode 100644 index 00000000..55029cd4 --- /dev/null +++ b/spec/views/programs_index.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'programs/index.html.haml' do + context 'programs without a username' do + it "renders the page without error" do + programs = [Fabricate(:program, author_username: nil)] + assign(:programs, programs) + render + rendered.should have_selector('div', id: 'title', contents: 'MyString') + end + end +end diff --git a/spec/views/questions/index.atom.builder_spec.rb b/spec/views/questions/index.atom.builder_spec.rb new file mode 100644 index 00000000..08d0faab --- /dev/null +++ b/spec/views/questions/index.atom.builder_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'questions/index.atom.builder' do + let(:question) { Fabricate(:question) } + + before { assign(:questions, [question]) } + + it "renders the feed without error" do + render + rendered.should include(question.title) + rendered.should include(question.description) + rendered.should include(question.updated_at.strftime("%Y-%m-%dT%H:%M:%SZ")) + rendered.should include(question.user.username) + end +end diff --git a/spec/views/questions/index.html.haml_spec.rb b/spec/views/questions/index.html.haml_spec.rb new file mode 100644 index 00000000..f371781c --- /dev/null +++ b/spec/views/questions/index.html.haml_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe 'questions/index.html.haml' do + let(:question) { Fabricate(:question) } + + before(:each) do + # stub the partials and test them individually + stub_template "shared/_ask" => "" + stub_template "questions/_list" => "" + + view.stub :will_paginate + end + + it "renders an autodiscovery link in for the head content" do + render :template => "questions/index.html.haml", :locals => {:collection => [question]} + + view.content_for(:head).should include(questions_url(format: :atom)) + end +end \ No newline at end of file diff --git a/vendor/assets/javascripts/prettify.js b/vendor/assets/javascripts/prettify.js new file mode 100644 index 00000000..eef5ad7e --- /dev/null +++ b/vendor/assets/javascripts/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p