├── log └── .gitkeep ├── lib ├── tasks │ ├── .gitkeep │ └── erd.rake └── assets │ └── .gitkeep ├── public ├── favicon.ico ├── partials │ ├── home.html │ ├── 404.html │ ├── login.html │ ├── questionFeed.html │ ├── addTopic.html │ ├── comment.html │ ├── addQuestion.html │ ├── topics.html │ ├── topic.html │ ├── question.html │ ├── updateProfile.html │ └── profile.html ├── robots.txt ├── scripts │ ├── app.js │ ├── config.js │ ├── res │ │ ├── angular-cookies.min.js │ │ ├── angular-resource.min.js │ │ ├── angular-route.min.js │ │ ├── ng-file-upload-shim.min.js │ │ ├── loading-bar.js │ │ ├── angular-ui-router.min.js │ │ └── ng-file-upload.min.js │ └── controllers │ │ ├── commentsController.js │ │ ├── sessionsController.js │ │ ├── questionsController.js │ │ ├── topicsController.js │ │ └── usersController.js ├── css │ ├── app.css │ └── loading-bar.css └── index.html ├── test ├── unit │ ├── .gitkeep │ ├── user_test.rb │ └── relationship_test.rb ├── fixtures │ ├── .gitkeep │ ├── relationships.yml │ └── users.yml ├── functional │ └── .gitkeep ├── integration │ └── .gitkeep ├── performance │ └── browsing_test.rb └── test_helper.rb ├── app ├── models │ ├── .gitkeep │ ├── question_topic.rb │ ├── comment.rb │ ├── relationship.rb │ ├── user_topic.rb │ ├── answer.rb │ ├── question.rb │ ├── topic.rb │ └── user.rb ├── views │ └── api │ │ └── v1 │ │ ├── users │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── _user.json.jbuilder │ │ ├── topics │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── _topic.json.jbuilder │ │ ├── answers │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── _answer.json.jbuilder │ │ ├── sessions │ │ └── show.json.jbuilder │ │ ├── comments │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── _comment.json.jbuilder │ │ └── questions │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── _question.json.jbuilder └── controllers │ ├── api │ └── v1 │ │ ├── comments_controller.rb │ │ ├── user_topics_controller.rb │ │ ├── question_topics_controller.rb │ │ ├── topics_controller.rb │ │ ├── relationships_controller.rb │ │ ├── answers_controller.rb │ │ ├── users_controller.rb │ │ ├── questions_controller.rb │ │ └── sessions_controller.rb │ └── application_controller.rb ├── vendor ├── plugins │ └── .gitkeep └── assets │ ├── javascripts │ └── .gitkeep │ └── stylesheets │ └── .gitkeep ├── erd.png ├── config.ru ├── config ├── environment.rb ├── boot.rb ├── initializers │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── secret_token.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── locales │ └── en.yml ├── routes.rb ├── database.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── db ├── migrate │ ├── 20160304091425_add_password_digest_to_users.rb │ ├── 20160304091724_add_remember_token_to_users.rb │ ├── 20160304092602_create_topics.rb │ ├── 20160304090930_create_users.rb │ ├── 20160304091307_add_avatar_to_users.rb │ ├── 20160304092313_create_questions.rb │ ├── 20160304092524_create_comments.rb │ ├── 20160304092441_create_answers.rb │ ├── 20160304092647_create_user_topics.rb │ ├── 20160304092624_create_question_topics.rb │ └── 20160304093836_create_relationships.rb ├── seeds.rb └── schema.rb ├── doc └── README_FOR_APP ├── Rakefile ├── script └── rails ├── README.md ├── Gemfile ├── .gitignore └── Gemfile.lock /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/functional/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksashikumar/QuoraClone/HEAD/erd.png -------------------------------------------------------------------------------- /app/views/api/v1/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/v1/users/user", user: @user 2 | -------------------------------------------------------------------------------- /app/views/api/v1/topics/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/v1/topics/topic', topic: @topic 2 | -------------------------------------------------------------------------------- /app/views/api/v1/answers/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/v1/answers/answer', answer: @answer 2 | -------------------------------------------------------------------------------- /app/views/api/v1/sessions/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/v1/users/user", user: current_user 2 | -------------------------------------------------------------------------------- /app/views/api/v1/comments/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/v1/comments/comment', comment: @comment 2 | -------------------------------------------------------------------------------- /app/views/api/v1/questions/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/v1/questions/question', question: @question 2 | -------------------------------------------------------------------------------- /app/views/api/v1/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @users do |user| 2 | json.partial! 'api/v1/users/user', user: user 3 | end 4 | -------------------------------------------------------------------------------- /app/views/api/v1/topics/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @topics do |topic| 2 | json.partial! 'api/v1/topics/topic', topic: topic 3 | end 4 | -------------------------------------------------------------------------------- /app/views/api/v1/answers/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @answers do |answer| 2 | json.partial! 'api/v1/answers/answer', answer: answer 3 | end 4 | -------------------------------------------------------------------------------- /app/views/api/v1/comments/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @comments do |comment| 2 | json.partial! 'api/v1/comments/comment', comment: comment 3 | end 4 | -------------------------------------------------------------------------------- /app/views/api/v1/questions/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @questions do |question| 2 | json.partial! 'api/v1/questions/question', question: question 3 | end 4 | 5 | -------------------------------------------------------------------------------- /test/unit/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/question_topic.rb: -------------------------------------------------------------------------------- 1 | class QuestionTopic < ActiveRecord::Base 2 | attr_accessible :question_id, :topic_id 3 | 4 | belongs_to :question 5 | belongs_to :topic 6 | 7 | end 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run QuoraClone::Application 5 | -------------------------------------------------------------------------------- /test/unit/relationship_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RelationshipTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/v1/comments/_comment.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! comment, :id, :body, :user_id, :answer_id, :updated_at 2 | 3 | json.user comment.user, :id, :first_name, :last_name, :email 4 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | QuoraClone::Application.initialize! 6 | -------------------------------------------------------------------------------- /public/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Welcome to QuoraClone

4 |

Made with Ruby on Rails and AngularJS

5 |
6 |
-------------------------------------------------------------------------------- /db/migrate/20160304091425_add_password_digest_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddPasswordDigestToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :password_digest, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /db/migrate/20160304091724_add_remember_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRememberTokenToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :remember_token, :string 4 | add_index :users, :remember_token 5 | end 6 | end -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/fixtures/relationships.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html 2 | 3 | one: 4 | follower_id: 1 5 | followed_id: 1 6 | 7 | two: 8 | follower_id: 1 9 | followed_id: 1 10 | -------------------------------------------------------------------------------- /public/scripts/app.js: -------------------------------------------------------------------------------- 1 | angular.module('QuoraClone', ['ui.bootstrap', 'ngRoute', 'ngCookies', 'ngFileUpload', 'angular-loading-bar']); 2 | String.prototype.capitalize = function() { 3 | return this.charAt(0).toUpperCase() + this.slice(1); 4 | } 5 | -------------------------------------------------------------------------------- /app/views/api/v1/topics/_topic.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! topic, :id, :title, :description 2 | 3 | json.questions do 4 | json.array! topic.questions do |question| 5 | json.partial! 'api/v1/questions/question', question: question 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /lib/tasks/erd.rake: -------------------------------------------------------------------------------- 1 | desc 'Generate Entity Relationship Diagram' 2 | task :generate_erd do 3 | system "erd --inheritance --filetype=dot --direct --attributes=foreign_keys,content" 4 | system "dot -Tpng erd.dot > erd.png" 5 | File.delete('erd.dot') 6 | end -------------------------------------------------------------------------------- /db/migrate/20160304092602_create_topics.rb: -------------------------------------------------------------------------------- 1 | class CreateTopics < ActiveRecord::Migration 2 | def change 3 | create_table :topics do |t| 4 | t.string :title 5 | t.text :description 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | attr_accessible :body, :answer_id, :updated_at 3 | 4 | belongs_to :user, foreign_key: :user_id 5 | belongs_to :answer, foreign_key: :answer_id 6 | 7 | validates :body, :answer_id, presence: true 8 | 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20160304090930_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.string :email 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/relationship.rb: -------------------------------------------------------------------------------- 1 | class Relationship < ActiveRecord::Base 2 | attr_accessible :followed_id 3 | 4 | belongs_to :follower, class_name: "User" 5 | belongs_to :followed, class_name: "User" 6 | 7 | validates :follower_id, presence: true 8 | validates :followed_id, presence: true 9 | end -------------------------------------------------------------------------------- /app/models/user_topic.rb: -------------------------------------------------------------------------------- 1 | class UserTopic < ActiveRecord::Base 2 | attr_accessible :topic_id, :user_id 3 | 4 | belongs_to :user 5 | belongs_to :topic 6 | 7 | validates :user, :topic, presence: true 8 | 9 | validates_uniqueness_of :user_id, :scope => :topic_id 10 | 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20160304091307_add_avatar_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAvatarToUsers < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.attachment :avatar 5 | end 6 | end 7 | 8 | def self.down 9 | drop_attached_file :users, :avatar 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html 2 | 3 | one: 4 | first_name: MyString 5 | last_name: MyString 6 | email: MyString 7 | 8 | two: 9 | first_name: MyString 10 | last_name: MyString 11 | email: MyString 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | QuoraClone::Application.load_tasks 8 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Quora Clone - Minimal Version of Quora in Rails 2 | 3 | [Demo](http://52.33.191.220/) 4 | 5 | ## FEATURES: 6 | 7 | * Models: Question, Answer, Relationship, Comments, User, Topics 8 | * API design 9 | * AngularJS 10 | 11 | ## Domain Model 12 | 13 | ![](/erd.png) 14 | 15 | _Made with love for Quoara_ :heart: -------------------------------------------------------------------------------- /app/views/api/v1/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :first_name, :last_name, :email, :topic_ids, :followers, :followed_users 2 | 3 | json.topics do 4 | json.array! user.topics do |topic| 5 | json.partial! 'api/v1/topics/topic', topic: topic 6 | end 7 | end 8 | 9 | json.avatar user.avatar.url 10 | json.success "true" 11 | -------------------------------------------------------------------------------- /app/models/answer.rb: -------------------------------------------------------------------------------- 1 | class Answer < ActiveRecord::Base 2 | attr_accessible :body, :question_id, :user_id, :votes 3 | 4 | belongs_to :user, foreign_key: :user_id 5 | belongs_to :question, foreign_key: :question_id 6 | 7 | has_many :comments, foreign_key: :answer_id 8 | 9 | validates :body, :question_id, presence: true 10 | 11 | end 12 | -------------------------------------------------------------------------------- /app/models/question.rb: -------------------------------------------------------------------------------- 1 | class Question < ActiveRecord::Base 2 | attr_accessible :description, :title, :votes, :user_id, :topics 3 | 4 | belongs_to :user, foreign_key: :user_id 5 | 6 | has_many :topics, :through => :question_topics 7 | has_many :question_topics 8 | 9 | has_many :answers 10 | 11 | validates :title, presence: true 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20160304092313_create_questions.rb: -------------------------------------------------------------------------------- 1 | class CreateQuestions < ActiveRecord::Migration 2 | def change 3 | create_table :questions do |t| 4 | t.text :title 5 | t.text :description 6 | t.integer :votes 7 | t.references :user 8 | 9 | t.timestamps 10 | end 11 | add_index :questions, :user_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20160304092524_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def change 3 | create_table :comments do |t| 4 | t.text :body 5 | t.references :user 6 | t.references :answer 7 | 8 | t.timestamps 9 | end 10 | add_index :comments, :user_id 11 | add_index :comments, :answer_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /db/migrate/20160304092441_create_answers.rb: -------------------------------------------------------------------------------- 1 | class CreateAnswers < ActiveRecord::Migration 2 | def change 3 | create_table :answers do |t| 4 | t.text :body 5 | t.integer :votes 6 | t.references :user 7 | t.references :question 8 | 9 | t.timestamps 10 | end 11 | add_index :answers, :user_id 12 | add_index :answers, :question_id 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/topic.rb: -------------------------------------------------------------------------------- 1 | class Topic < ActiveRecord::Base 2 | attr_accessible :description, :title 3 | 4 | has_many :question_topics 5 | has_many :questions, :through => :question_topics 6 | 7 | has_many :user_topics 8 | has_many :users, through: :user_topics 9 | 10 | before_save do |topic| 11 | topic.title.downcase! 12 | end 13 | 14 | validates :title, presence: true, uniqueness: true 15 | 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20160304092647_create_user_topics.rb: -------------------------------------------------------------------------------- 1 | class CreateUserTopics < ActiveRecord::Migration 2 | def change 3 | create_table :user_topics do |t| 4 | t.references :user 5 | t.references :topic 6 | 7 | t.timestamps 8 | end 9 | add_index :user_topics, :user_id 10 | add_index :user_topics, :topic_id 11 | add_index :user_topics, [:user_id, :topic_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/api/v1/answers/_answer.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! answer, :id, :body, :question_id, :user_id, :updated_at 2 | 3 | json.question answer.question, :id, :title, :description 4 | 5 | json.comments do 6 | json.array! answer.comments do |comment| 7 | json.partial! 'api/v1/comments/comment', 8 | comment: comment 9 | end 10 | end 11 | 12 | json.user answer.user, :id, :first_name, :last_name 13 | 14 | json.votes answer.votes 15 | -------------------------------------------------------------------------------- /test/performance/browsing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/performance_test_help' 3 | 4 | class BrowsingTest < ActionDispatch::PerformanceTest 5 | # Refer to the documentation for all available options 6 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] 7 | # :output => 'tmp/performance', :formats => [:flat] } 8 | 9 | def test_homepage 10 | get '/' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /db/migrate/20160304092624_create_question_topics.rb: -------------------------------------------------------------------------------- 1 | class CreateQuestionTopics < ActiveRecord::Migration 2 | def change 3 | create_table :question_topics do |t| 4 | t.references :question 5 | t.references :topic 6 | 7 | t.timestamps 8 | end 9 | add_index :question_topics, :question_id 10 | add_index :question_topics, :topic_id 11 | add_index :question_topics, [:question_id, :topic_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20160304093836_create_relationships.rb: -------------------------------------------------------------------------------- 1 | class CreateRelationships < ActiveRecord::Migration 2 | def change 3 | create_table :relationships do |t| 4 | t.integer :follower_id 5 | t.integer :followed_id 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :relationships, :follower_id 11 | add_index :relationships, :followed_id 12 | add_index :relationships, [:follower_id, :followed_id], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '3.2.18' 4 | gem 'mysql' 5 | 6 | 7 | group :assets do 8 | gem 'sass-rails', '~> 3.2.3' 9 | gem 'coffee-rails', '~> 3.2.1' 10 | gem 'uglifier', '>= 1.0.3' 11 | end 12 | 13 | gem 'jquery-rails' 14 | gem 'jbuilder' 15 | gem 'byebug' 16 | gem 'jquery-ui-rails' 17 | gem 'paperclip', '~> 4.1' 18 | gem 'strong_parameters' 19 | gem 'bcrypt-ruby', '3.0.1' 20 | gem 'rails-erd' 21 | gem 'brakeman', :require => false -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | QuoraClone::Application.config.session_store :cookie_store, key: '_QuoraClone_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # QuoraClone::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /app/views/api/v1/questions/_question.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! question, :id, :title, :description, :user_id, :updated_at 2 | 3 | json.user question.user, :id, :first_name, :last_name, :email 4 | 5 | json.answers do 6 | json.array! question.answers do |answer| 7 | json.partial! 'api/v1/answers/answer', answer: answer 8 | end 9 | end 10 | 11 | json.topics do 12 | json.array! question.topics do |topic| 13 | json.extract! topic, :id, :title, :description 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/*.log 12 | /tmp 13 | 14 | /public/system 15 | 16 | .byebug_history 17 | .DS_Store -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. 7 | # 8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 9 | # -- they do not yet inherit this setting 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | QuoraClone::Application.config.secret_token = '29a04dd2d5a0ace95df583380d18e9e5286acfd26d83cc73f5d7459a1b6b1822a98e1f653240b6c9e0b4af9dd03a033c81127db4213526f8bbee76e3c28279bf' 8 | -------------------------------------------------------------------------------- /public/partials/404.html: -------------------------------------------------------------------------------- 1 | 2 | The page you were looking for doesn't exist (404) 3 | 7 | 8 | 9 | 10 |
11 |

The page you were looking for doesn't exist.

12 |

You may have mistyped the address or the page may have moved.

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /app/controllers/api/v1/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::CommentsController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @comment = Comment.new(comment_params) 7 | @comment.user_id = current_user.id 8 | 9 | if @comment.save 10 | render json: @comment 11 | else 12 | render json: @comment.errors.full_messages, status: :unprocessable_entity 13 | end 14 | end 15 | 16 | def index 17 | @comments = Answer.find_by_id(comment_params[:answer_id]).comments 18 | render :index 19 | end 20 | 21 | def show 22 | @comment = Comment.includes(:answer).find(params[:id]) 23 | render :show 24 | end 25 | 26 | def comment_params 27 | params.require(:comment).permit(:answer_id, :body) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/api/v1/user_topics_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UserTopicsController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @user_topic = UserTopic.new( 7 | :topic_id => params["topic"]["id"], 8 | :user_id => current_user.id 9 | ) 10 | if @user_topic.save 11 | render json: @user_topic 12 | else 13 | render json: @user_topic.errors.full_messages, 14 | status: :unprocessable_entity 15 | end 16 | end 17 | 18 | def index 19 | @user_topics = UserTopic.all 20 | render json: @user_topics 21 | end 22 | 23 | def show 24 | @user_topic = UserTopic.find(params[:id]) 25 | render :show 26 | end 27 | 28 | def user_topic_params 29 | params.require(:topic).permit(:user_id, :topic_id) 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/api/v1/question_topics_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::QuestionTopicsController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @question_topic = QuestionTopic.new(question_topic_params) 7 | 8 | if @question_topic.save 9 | render json: @question_topic 10 | else 11 | render json: @question_topic.errors.full_messages, 12 | status: :unprocessable_entity 13 | end 14 | end 15 | 16 | def index 17 | @question_topics = QuestionTopic.includes(:question, :topic).all 18 | render json: @question_topics 19 | end 20 | 21 | def show 22 | @question_topic = QuestionTopic.includes(:question, :topic). 23 | find(params[:id]) 24 | render :show 25 | end 26 | 27 | def question_topic_params 28 | params.require(:question_topic).permit(:question_id, :topic_id) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/api/v1/topics_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::TopicsController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @topic = Topic.new(topic_params) 7 | 8 | if @topic.save 9 | render json: @topic 10 | else 11 | render json: @topic.errors.full_messages, status: :unprocessable_entity 12 | end 13 | end 14 | 15 | def index 16 | @topics = Topic.all 17 | render :index 18 | end 19 | 20 | def show 21 | @topic = Topic.find(params["topic"]["id"]) 22 | render :show 23 | end 24 | 25 | def users_topics 26 | @topics = User.find_by_id(params["topic"]["user_id"]).topics 27 | render :index 28 | end 29 | 30 | def topic_name_index 31 | @topics = Topic.all 32 | render :topic_name_index 33 | end 34 | 35 | def topic_params 36 | params.require(:topic).permit(:title, :description) 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /app/controllers/api/v1/relationships_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::RelationshipsController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @user = User.find(params["relationships"]["followed_id"]) 7 | if current_user.follow!(@user) 8 | render :status => 200, 9 | :json => 10 | { 11 | :success => true 12 | } 13 | else 14 | render :status => 401, 15 | :json => 16 | { 17 | :success => false 18 | } 19 | end 20 | end 21 | 22 | def destroy 23 | @user = User.find(params["id"]) 24 | if current_user.unfollow!(@user) 25 | render :status => 200, 26 | :json => 27 | { 28 | :success => true 29 | } 30 | else 31 | render :status => 401, 32 | :json => 33 | { 34 | :success => false 35 | } 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /app/controllers/api/v1/answers_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::AnswersController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @answer = Answer.new(answer_params) 7 | @answer.user_id = current_user.id 8 | 9 | if @answer.save 10 | render json: @answer 11 | else 12 | render json: @answer.errors.full_messages, status: :unprocessable_entity 13 | end 14 | end 15 | 16 | def destroy 17 | @answer = Answer.find(params[:id]) 18 | @answer.try(:destroy) 19 | render json: {} 20 | end 21 | 22 | def index 23 | @answers = Answer.includes(:question, :comments).all 24 | render :index 25 | end 26 | 27 | def show 28 | @answer = Answer.includes(:question, :comments).find(params[:id]) 29 | render :show 30 | end 31 | 32 | def answer_user 33 | @answers = User.find_by_id(params["user"]["id"]).answers 34 | 35 | render :index 36 | end 37 | 38 | def answer_params 39 | params.require(:answer).permit(:body, :question_id, :votes) 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /public/partials/login.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 |

Please sign in

10 | 11 | 19 | 20 | 21 | 29 | 30 | 31 | 32 |
33 |
-------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | QuoraClone::Application.routes.draw do 2 | 3 | namespace :api, defaults: { format: :json } do 4 | namespace :v1 do 5 | 6 | get 'sessions' => 'sessions#user_signed_in' 7 | 8 | post 'show_user' => 'users#show' 9 | 10 | post 'answer_user' => 'answers#answer_user' 11 | 12 | post 'question_feed' => 'questions#feed' 13 | post 'question_user' => 'questions#question_user' 14 | post 'question' => 'questions#show' 15 | post 'get_comments' => 'comments#index' 16 | post 'show_topic' => 'topics#show' 17 | post 'topic' => 'topics#create' 18 | post 'follow_topic' => 'user_topics#create' 19 | post 'user_topics' => 'topics#users_topics' 20 | 21 | resources :questions 22 | resources :answers 23 | resources :comments 24 | resources :topics 25 | resources :question_topics 26 | resources :user_topics 27 | resources :sessions 28 | resources :relationships 29 | 30 | resources :users do 31 | member do 32 | get :following, :followers 33 | end 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # MySQL. Versions 4.1 and 5.0 are recommended. 2 | # 3 | # Install the MYSQL driver 4 | # gem install mysql2 5 | # 6 | # Ensure the MySQL gem is defined in your Gemfile 7 | # gem 'mysql2' 8 | # 9 | # And be sure to use new-style password hashing: 10 | # http://dev.mysql.com/doc/refman/5.0/en/old-client.html 11 | development: 12 | adapter: mysql 13 | encoding: utf8 14 | reconnect: false 15 | database: QuoraClone_development 16 | pool: 5 17 | username: root 18 | password: 19 | socket: /tmp/mysql.sock 20 | 21 | # Warning: The database defined as "test" will be erased and 22 | # re-generated from your development database when you run "rake". 23 | # Do not set this db to the same as development or production. 24 | test: 25 | adapter: mysql 26 | encoding: utf8 27 | reconnect: false 28 | database: QuoraClone_test 29 | pool: 5 30 | username: root 31 | password: 32 | socket: /tmp/mysql.sock 33 | 34 | production: 35 | adapter: mysql 36 | encoding: utf8 37 | reconnect: false 38 | database: QuoraClone_production 39 | pool: 5 40 | username: root 41 | password: 42 | socket: /tmp/mysql.sock 43 | -------------------------------------------------------------------------------- /public/partials/questionFeed.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 |

See all Topics

10 |
11 | 12 | 13 |
14 |

{{question.title}}

15 |

Description: {{question.description}}

16 |

Answers: {{question.answers.length}}

17 |

Topics: {{question.topics.length}} 18 |

Votes: {{question.votes}} 19 |

Asked By: {{question.user.first_name}} on {{question.updated_at | date: 'medium'}}

20 |
21 | 22 |
-------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | # Force signout to prevent CSRF attacks 5 | def handle_unverified_request 6 | #sign_out 7 | super 8 | end 9 | 10 | def sign_in(user) 11 | cookies.permanent[:remember_token] = user.remember_token 12 | current_user = user 13 | end 14 | 15 | def current_user=(user) 16 | @current_user = user 17 | end 18 | 19 | def current_user 20 | @current_user ||= user_from_remember_token 21 | end 22 | 23 | def current_user?(user) 24 | user == current_user 25 | end 26 | 27 | def signed_in? 28 | !current_user.nil? 29 | end 30 | 31 | def sign_out 32 | current_user = nil 33 | cookies.delete(:remember_token) 34 | end 35 | 36 | def store_location 37 | session[:return_to] = request.fullpath 38 | end 39 | 40 | private 41 | 42 | def user_from_remember_token 43 | remember_token = cookies[:remember_token] 44 | User.find_by_remember_token(remember_token) unless remember_token.nil? 45 | end 46 | 47 | def clear_return_to 48 | session.delete(:return_to) 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /public/scripts/config.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('QuoraClone') 3 | .config(['$routeProvider', function($routeProvider) { 4 | $routeProvider 5 | .when("/", {templateUrl: "partials/home.html"}) 6 | .when("/login", {templateUrl: "partials/login.html"}) 7 | .when("/signup", {templateUrl: "partials/updateProfile.html"}) 8 | .when("/profile/:id", {templateUrl: "partials/profile.html"}) 9 | .when("/addQuestion", {templateUrl: "partials/addQuestion.html"}) 10 | .when("/question/:id", {templateUrl: "partials/question.html"}) 11 | .when("/questionFeed", {templateUrl: "partials/questionFeed.html"}) 12 | .when('/:answerId/comments', {templateUrl: "partials/comment.html"}) 13 | .when('/topics/:id', {templateUrl: "partials/topic.html"}) 14 | .when('/topics', {templateUrl: "partials/topics.html"}) 15 | .when('/topic/create', {'templateUrl': 'partials/addTopic.html'}) 16 | .when("/404", {templateUrl: "partials/404.html"}) 17 | .otherwise({ 18 | redirectTo: '/404' 19 | }) 20 | }]) 21 | .config(['cfpLoadingBarProvider', function(cfpLoadingBarProvider) { 22 | cfpLoadingBarProvider.includeBar = true; 23 | cfpLoadingBarProvider.includeSpinner = false; 24 | }]); 25 | 26 | -------------------------------------------------------------------------------- /public/partials/addTopic.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 | × 10 | {{success}} 11 |
12 | 13 |
14 |

Create Topic

15 | 16 | 24 | 25 | 26 | 33 | 34 | 35 | 36 |
37 |
-------------------------------------------------------------------------------- /public/partials/comment.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 | × 10 | {{success}} 11 |
12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |

{{comments.length}} Comments

24 |
25 |

{{comment.body}}

26 |

27 | Commented By: {{comment.user.first_name}} 28 | on {{comment.updated_at | date: 'medium'}}

29 |

30 |
31 |
32 | 33 |
-------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApplicationController 2 | 3 | before_filter :signed_in?, 4 | only: [:index, :edit, :update] 5 | 6 | def index 7 | @users = User.all 8 | end 9 | 10 | def create 11 | @user = User.new(user_params) 12 | if @user.save 13 | sign_in @user 14 | render :status => 200, 15 | :json => 16 | { 17 | :success => true, 18 | :info => "Signed Up", 19 | :data => { 20 | :remember_token => current_user.remember_token, 21 | :user_id => current_user.id 22 | } 23 | } 24 | else 25 | render :status => 401, 26 | :json => 27 | { 28 | :success => false, 29 | :info => "Signup Error", 30 | :data => { } 31 | } 32 | end 33 | end 34 | 35 | def show 36 | @user = User.find(user_params[:id]) 37 | render :show 38 | end 39 | 40 | def new 41 | @user = User.new 42 | end 43 | 44 | def user_params 45 | params.require(:user).permit(:id, :first_name, :last_name, 46 | :email, :password, :password_confirmation, 47 | :avatar) 48 | end 49 | 50 | end -------------------------------------------------------------------------------- /public/scripts/res/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.4.9 3 | (c) 2010-2015 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(p,c,n){'use strict';function l(b,a,g){var d=g.baseHref(),k=b[0];return function(b,e,f){var g,h;f=f||{};h=f.expires;g=c.isDefined(f.path)?f.path:d;c.isUndefined(e)&&(h="Thu, 01 Jan 1970 00:00:00 GMT",e="");c.isString(h)&&(h=new Date(h));e=encodeURIComponent(b)+"="+encodeURIComponent(e);e=e+(g?";path="+g:"")+(f.domain?";domain="+f.domain:"");e+=h?";expires="+h.toUTCString():"";e+=f.secure?";secure":"";f=e.length+1;4096 4096 bytes)!");k.cookie=e}}c.module("ngCookies",["ng"]).provider("$cookies",[function(){var b=this.defaults={};this.$get=["$$cookieReader","$$cookieWriter",function(a,g){return{get:function(d){return a()[d]},getObject:function(d){return(d=this.get(d))?c.fromJson(d):d},getAll:function(){return a()},put:function(d,a,m){g(d,a,m?c.extend({},b,m):b)},putObject:function(d,b,a){this.put(d,c.toJson(b),a)},remove:function(a,k){g(a,n,k?c.extend({},b,k):b)}}}]}]);c.module("ngCookies").factory("$cookieStore", 8 | ["$cookies",function(b){return{get:function(a){return b.getObject(a)},put:function(a,c){b.putObject(a,c)},remove:function(a){b.remove(a)}}}]);l.$inject=["$document","$log","$browser"];c.module("ngCookies").provider("$$cookieWriter",function(){this.$get=l})})(window,window.angular); 9 | //# sourceMappingURL=angular-cookies.min.js.map 10 | -------------------------------------------------------------------------------- /public/partials/addQuestion.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 |

Ask a Question

10 | 11 | 19 | 20 | 21 | 28 | 29 | 30 | 37 | 38 | 39 | 40 |
41 |
-------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | QuoraClone::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /public/scripts/controllers/commentsController.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('QuoraClone') 3 | .controller('commentsController', function($scope, $http, $location, $cookieStore, $routeParams) { 4 | 5 | $scope.answerId = $routeParams.answerId; 6 | $scope.comment = {}; 7 | $scope.comment.comment = {}; 8 | $scope.error = null; 9 | $scope.success = null; 10 | 11 | $scope.init = function() { 12 | 13 | $scope.comment.comment.answer_id = $scope.answerId; 14 | 15 | $http({ 16 | method : 'POST', 17 | url : 'api/v1/get_comments', 18 | data : $scope.comment, 19 | headers : {'Content-Type': 'application/json'} 20 | }) 21 | .success(function(data) { 22 | $scope.comments = data; 23 | console.log(data); 24 | }) 25 | .error(function(data) { 26 | $scope.error = "Comment Fetch Error!" 27 | }) 28 | 29 | } 30 | 31 | $scope.submitComment = function() { 32 | $scope.comment.comment.answer_id = $scope.answerId; 33 | 34 | $http({ 35 | method : 'POST', 36 | url : 'api/v1/comments', 37 | data : $scope.comment, 38 | headers : {'Content-Type': 'application/json'} 39 | }) 40 | .success(function(data) { 41 | $scope.success = "Comment Added Successfully!"; 42 | }) 43 | .error(function(data) { 44 | $scope.error = "Cannot Comment!"; 45 | }) 46 | } 47 | 48 | $scope.showUser = function(id) { 49 | $location.path('/profile/' + id) 50 | } 51 | 52 | 53 | }); -------------------------------------------------------------------------------- /public/partials/topics.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 | × 10 | {{success}} 11 |
12 | 13 |
14 |
15 |
16 |

{{topics.length}} Topics

17 |
18 | 19 |
20 |

21 |
22 |
23 | 24 |
25 |
26 |

{{topic.title.capitalize()}}

27 |

Description: {{topic.description}}

28 |

{{topic.questions.length}} Questions

29 |
30 |
31 |

32 |
33 |
34 | 35 |
36 | 37 |
-------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | QuoraClone::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /public/partials/topic.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 | × 10 | {{success}} 11 |
12 | 13 | 14 |
15 |
16 |

{{topic.title.capitalize()}}

17 |

Description: {{topic.description}}

18 |
19 |
20 |

21 |
22 |
23 | 24 |
25 |

No Questions in this topic

26 |
27 | 28 |
29 |
30 |

{{topic.questions.length}} Questions

31 |
32 |

{{question.title}}

33 |

Description: {{question.description}}

34 |

Answers: {{question.answers.length}}

35 |

Topics: {{question.topics.length}} 36 |

Votes: {{question.votes}} 37 |

Asked By: {{question.user.first_name}} on {{question.updated_at | date: 'medium'}}

38 |
39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /app/controllers/api/v1/questions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::QuestionsController < ApplicationController 2 | 3 | before_filter :signed_in? 4 | 5 | def create 6 | @question = Question.new(question_params) 7 | @question.user_id = current_user.id 8 | @question.votes = question_params[:votes] 9 | 10 | all_topics = Topic.pluck(:title) 11 | all_topic_ids = Topic.pluck(:id) 12 | 13 | topics = params["question"]["topics"] 14 | 15 | if topics.nil? 16 | topics = [] 17 | end 18 | 19 | topic_ids = [] 20 | 21 | topics.each do |topic| 22 | index = all_topics.index(topic) 23 | if !index.nil? 24 | topic_ids << all_topic_ids[index] + 1 25 | else 26 | new_topic = Topic.new(:title => topic) 27 | new_topic.save! 28 | topic_ids << new_topic.id 29 | end 30 | end 31 | 32 | if !topic_ids.empty? 33 | @question.topic_ids = topic_ids 34 | end 35 | 36 | if @question.save 37 | render :status => 200, 38 | :json => 39 | { 40 | :success => true, 41 | :info => "Question Added", 42 | :data => { 43 | :id => @question.id 44 | } 45 | } 46 | else 47 | render status: :unprocessable_entity 48 | end 49 | end 50 | 51 | def feed 52 | @questions = [] 53 | @topics = User.find_by_id(question_params[:user_id]).topics 54 | @topics.each do |topic| 55 | topic.questions.each do |que| 56 | if que.user != current_user 57 | @questions << que 58 | end 59 | end 60 | end 61 | render :index 62 | end 63 | 64 | def question_user 65 | @questions = User.find_by_id(params["question"]["user_id"]).questions 66 | render :index 67 | end 68 | 69 | def show 70 | @question = Question.find(params["question"]["id"]) 71 | @question.answers.reverse! 72 | render :show 73 | end 74 | 75 | def question_params 76 | params.require(:question).permit(:title, :description, :votes, :user_id) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | attr_accessible :first_name, :last_name, :email, 3 | :password, :followers, :followed_users, :avatar 4 | has_secure_password 5 | 6 | before_save :create_remember_token 7 | 8 | validates :first_name, presence: true, length: { maximum: 50 } 9 | validates :last_name, presence: true, length: { maximum: 50 } 10 | 11 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i 12 | validates :email, presence: true, 13 | format: { with: VALID_EMAIL_REGEX }, 14 | uniqueness: { case_sensitive: false } 15 | 16 | validates :password, length: { minimum: 6 } 17 | 18 | has_many :relationships, foreign_key: "follower_id", dependent: :destroy 19 | has_many :followed_users, through: :relationships, source: :followed 20 | 21 | has_many :reverse_relationships, foreign_key: "followed_id", 22 | class_name: "Relationship", 23 | dependent: :destroy 24 | has_many :followers, through: :reverse_relationships, source: :follower 25 | 26 | has_attached_file :avatar, 27 | :default_url => "http://www.hovingpartners.ch/wp-content/uploads/2015/11/img-profile-missing.png" 28 | 29 | validates_attachment_content_type :avatar, :content_type => /\Aimage\/.*\Z/ 30 | 31 | has_many :answers, foreign_key: :user_id 32 | has_many :questions, foreign_key: :user_id 33 | has_many :comments, foreign_key: :user_id 34 | 35 | has_many :user_topics 36 | has_many :topics, through: :user_topics 37 | 38 | 39 | def following?(other_user) 40 | relationships.find_by_followed_id(other_user.id) 41 | end 42 | 43 | def follow!(other_user) 44 | relationships.create!(followed_id: other_user.id) 45 | end 46 | 47 | def unfollow!(other_user) 48 | relationships.find_by_followed_id(other_user.id).destroy 49 | end 50 | 51 | private 52 | 53 | def create_remember_token 54 | self.remember_token = SecureRandom.urlsafe_base64 55 | end 56 | end -------------------------------------------------------------------------------- /app/controllers/api/v1/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::SessionsController < ApplicationController 2 | 3 | def create 4 | user = User.find_by_email(session_params[:email]) 5 | if user && user.authenticate(session_params[:password]) 6 | sign_in user 7 | render :status => 200, 8 | :json => 9 | { 10 | :success => true, 11 | :info => "Logged in", 12 | :data => { 13 | :user_id => current_user.id, 14 | :remember_token => current_user.remember_token 15 | } 16 | } 17 | else 18 | render :status => 401, 19 | :json => 20 | { 21 | :success => false, 22 | :info => "Invalid email or password", 23 | :data => { } 24 | } 25 | end 26 | end 27 | 28 | def destroy 29 | if !current_user.nil? 30 | sign_out 31 | render :status => 200, 32 | :json => 33 | { 34 | :success => true, 35 | :info => "Logged out", 36 | :data => { } 37 | } 38 | else 39 | render :status => 401, 40 | :json => 41 | { 42 | :success => false, 43 | :info => "Not Accessible", 44 | :data => { } 45 | } 46 | end 47 | 48 | end 49 | 50 | def user_signed_in 51 | if !current_user.nil? 52 | render :status => 200, 53 | :json => 54 | { 55 | :success => true, 56 | :info => "Logged out", 57 | :data => { } 58 | } 59 | else 60 | render :status => 401, 61 | :json => 62 | { 63 | :success => false, 64 | :info => "Not Logged in", 65 | :data => { } 66 | } 67 | end 68 | end 69 | 70 | def session_params 71 | params.require(:user).permit(:email, :password, :session) 72 | end 73 | 74 | end -------------------------------------------------------------------------------- /public/partials/question.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 | × 10 | {{success}} 11 |
12 | 13 | 14 |
15 |
16 |
17 |

{{question.title}}

18 |

Description: {{question.description}}

19 |

Asked By: {{question.user.first_name}} on {{question.updated_at | date: 'medium'}}

20 |
21 |
22 |
23 |
24 |

Topics:

25 |
26 | {{topic.title}} 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 |

{{question.answers.length}} Answers

43 |
44 |

{{answer.body}}

45 |

46 | Answered By: {{answer.user.first_name}} 47 | on {{answer.updated_at | date: 'medium'}}

48 |

49 |

50 | {{answer.comments.length}} Comments 51 |

52 |
53 |
54 | 55 |
56 | -------------------------------------------------------------------------------- /public/partials/updateProfile.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | × 5 | Error! {{error}} 6 |
7 | 8 |
9 |

Signup

10 | 11 | 12 | 20 | 21 | 22 | 30 | 31 | 32 | 40 | 41 | 42 | 50 | 51 | 52 | 60 | 61 | 62 | 73 | 74 | 75 | 76 |
77 |
-------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | .form-app { 2 | max-width: 330px; 3 | padding: 15px; 4 | margin: 0 auto; 5 | } 6 | .form-app .form-app-heading { 7 | margin-bottom: 10px; 8 | } 9 | .form-app .form-control { 10 | position: relative; 11 | height: auto; 12 | -webkit-box-sizing: border-box; 13 | -moz-box-sizing: border-box; 14 | box-sizing: border-box; 15 | padding: 10px; 16 | font-size: 16px; 17 | } 18 | .form-app .form-control:focus { 19 | z-index: 2; 20 | } 21 | .form-app input[type="name"] { 22 | margin-bottom: 10px; 23 | border-bottom-right-radius: 0; 24 | border-bottom-left-radius: 0; 25 | } 26 | .form-app input[type="email"] { 27 | margin-bottom: 10px; 28 | border-bottom-right-radius: 0; 29 | border-bottom-left-radius: 0; 30 | } 31 | .form-app input[type="password"] { 32 | margin-bottom: 10px; 33 | border-top-left-radius: 0; 34 | border-top-right-radius: 0; 35 | } 36 | 37 | .form-app input[type="questionTitle"] { 38 | margin-bottom: 10px; 39 | border-top-left-radius: 0; 40 | border-top-right-radius: 0; 41 | } 42 | .form-app input[type="questionDesc"] { 43 | margin-bottom: 10px; 44 | border-top-left-radius: 0; 45 | border-top-right-radius: 0; 46 | } 47 | .form-app input[type="questionTopics"] { 48 | margin-bottom: 10px; 49 | border-top-left-radius: 0; 50 | border-top-right-radius: 0; 51 | } 52 | 53 | .form-app input[type="topicTitle"] { 54 | margin-bottom: 10px; 55 | border-top-left-radius: 0; 56 | border-top-right-radius: 0; 57 | } 58 | .form-app input[type="topicDesc"] { 59 | margin-bottom: 10px; 60 | border-top-left-radius: 0; 61 | border-top-right-radius: 0; 62 | } 63 | 64 | .vcard-stat { 65 | float: left; 66 | font-size: 11px; 67 | padding: 20px; 68 | text-transform: capitalize; 69 | } 70 | .vcard-stat-count { 71 | font-size: 28px; 72 | font-weight: bold; 73 | line-height: 1; 74 | text-align: center; 75 | } 76 | 77 | .vcard-follow { 78 | float: center; 79 | padding-left: 75px; 80 | } 81 | 82 | .text-muted { 83 | color: #767676; 84 | } 85 | 86 | .vcard-names { 87 | line-height: 1; 88 | } 89 | 90 | .avatar { 91 | margin-top: -1px; 92 | margin-right: 2px; 93 | border-radius: 2px; 94 | } 95 | 96 | .avatar-upload { 97 | margin-bottom: 10px; 98 | padding-bottom: 10px; 99 | border-top-left-radius: 0; 100 | border-top-right-radius: 0; 101 | } 102 | 103 | 104 | .right-button { 105 | float: left; 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | QuoraClone::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | if defined?(Bundler) 6 | # If you precompile assets before deploying to production, use this line 7 | Bundler.require(*Rails.groups(:assets => %w(development test))) 8 | # If you want your assets lazily compiled in production, use this line 9 | # Bundler.require(:default, :assets, Rails.env) 10 | end 11 | 12 | module QuoraClone 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | # config.autoload_paths += %W(#{config.root}/extras) 20 | 21 | # Only load the plugins named here, in the order given (default is alphabetical). 22 | # :all can be used as a placeholder for all plugins not explicitly named. 23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 24 | 25 | # Activate observers that should always be running. 26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 27 | 28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 30 | # config.time_zone = 'Central Time (US & Canada)' 31 | 32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 34 | # config.i18n.default_locale = :de 35 | 36 | # Configure the default encoding used in templates for Ruby 1.9. 37 | config.encoding = "utf-8" 38 | 39 | # Configure sensitive parameters which will be filtered from the log file. 40 | config.filter_parameters += [:password] 41 | 42 | # Enable escaping HTML in JSON. 43 | config.active_support.escape_html_entities_in_json = true 44 | 45 | # Use SQL instead of Active Record's schema dumper when creating the database. 46 | # This is necessary if your schema can't be completely dumped by the schema dumper, 47 | # like if you have constraints or database-specific column types 48 | # config.active_record.schema_format = :sql 49 | 50 | # Enforce whitelist mode for mass assignment. 51 | # This will create an empty whitelist of attributes available for mass-assignment for all models 52 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 53 | # parameters by using an attr_accessible or attr_protected declaration. 54 | config.active_record.whitelist_attributes = true 55 | 56 | # Enable the asset pipeline 57 | config.assets.enabled = true 58 | 59 | # Version of your assets, change this if you want to expire all your assets 60 | config.assets.version = '1.0' 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /public/css/loading-bar.css: -------------------------------------------------------------------------------- 1 | 2 | /* Make clicks pass-through */ 3 | #loading-bar, 4 | #loading-bar-spinner { 5 | pointer-events: none; 6 | -webkit-pointer-events: none; 7 | -webkit-transition: 350ms linear all; 8 | -moz-transition: 350ms linear all; 9 | -o-transition: 350ms linear all; 10 | transition: 350ms linear all; 11 | } 12 | 13 | #loading-bar.ng-enter, 14 | #loading-bar.ng-leave.ng-leave-active, 15 | #loading-bar-spinner.ng-enter, 16 | #loading-bar-spinner.ng-leave.ng-leave-active { 17 | opacity: 0; 18 | } 19 | 20 | #loading-bar.ng-enter.ng-enter-active, 21 | #loading-bar.ng-leave, 22 | #loading-bar-spinner.ng-enter.ng-enter-active, 23 | #loading-bar-spinner.ng-leave { 24 | opacity: 1; 25 | } 26 | 27 | #loading-bar .bar { 28 | -webkit-transition: width 350ms; 29 | -moz-transition: width 350ms; 30 | -o-transition: width 350ms; 31 | transition: width 350ms; 32 | 33 | background: #29d; 34 | position: fixed; 35 | z-index: 10002; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 2px; 40 | border-bottom-right-radius: 1px; 41 | border-top-right-radius: 1px; 42 | } 43 | 44 | /* Fancy blur effect */ 45 | #loading-bar .peg { 46 | position: absolute; 47 | width: 70px; 48 | right: 0; 49 | top: 0; 50 | height: 2px; 51 | opacity: .45; 52 | -moz-box-shadow: #29d 1px 0 6px 1px; 53 | -ms-box-shadow: #29d 1px 0 6px 1px; 54 | -webkit-box-shadow: #29d 1px 0 6px 1px; 55 | box-shadow: #29d 1px 0 6px 1px; 56 | -moz-border-radius: 100%; 57 | -webkit-border-radius: 100%; 58 | border-radius: 100%; 59 | } 60 | 61 | #loading-bar-spinner { 62 | display: block; 63 | position: fixed; 64 | z-index: 10002; 65 | top: 10px; 66 | left: 10px; 67 | } 68 | 69 | #loading-bar-spinner .spinner-icon { 70 | width: 14px; 71 | height: 14px; 72 | 73 | border: solid 2px transparent; 74 | border-top-color: #29d; 75 | border-left-color: #29d; 76 | border-radius: 50%; 77 | 78 | -webkit-animation: loading-bar-spinner 400ms linear infinite; 79 | -moz-animation: loading-bar-spinner 400ms linear infinite; 80 | -ms-animation: loading-bar-spinner 400ms linear infinite; 81 | -o-animation: loading-bar-spinner 400ms linear infinite; 82 | animation: loading-bar-spinner 400ms linear infinite; 83 | } 84 | 85 | @-webkit-keyframes loading-bar-spinner { 86 | 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 87 | 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } 88 | } 89 | @-moz-keyframes loading-bar-spinner { 90 | 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } 91 | 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } 92 | } 93 | @-o-keyframes loading-bar-spinner { 94 | 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } 95 | 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } 96 | } 97 | @-ms-keyframes loading-bar-spinner { 98 | 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } 99 | 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } 100 | } 101 | @keyframes loading-bar-spinner { 102 | 0% { transform: rotate(0deg); } 103 | 100% { transform: rotate(360deg); } 104 | } 105 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Quora Clone 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /public/scripts/controllers/sessionsController.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('QuoraClone') 3 | .controller('sessionsController',function ($scope, $http, $location, $cookieStore, $routeParams, Upload) { 4 | $scope.user = {}; 5 | $scope.newUser = {}; 6 | $scope.isLogin = false; 7 | 8 | $scope.init = function() { 9 | $http({ 10 | method : 'GET', 11 | url : 'api/v1/sessions' 12 | }) 13 | .success(function(data) { 14 | $scope.isLogin = true; 15 | return true; 16 | }) 17 | .error(function(data) { 18 | $scope.isLogin = false; 19 | return false; 20 | }) 21 | 22 | } 23 | 24 | $scope.showSelf = function() { 25 | $location.path("/profile/" + $cookieStore.get('userId')); 26 | } 27 | 28 | $scope.login = function() { 29 | 30 | $http({ 31 | method : 'POST', 32 | url : 'api/v1/sessions', 33 | data : $scope.user, 34 | headers : {'Content-Type': 'application/json'} 35 | }) 36 | .success(function(data) { 37 | if(data.success) { 38 | $cookieStore.put('rememberToken', data.data.remember_token); 39 | $cookieStore.put('userId', data.data.user_id); 40 | $scope.isLogin = true; 41 | $location.path('/'); 42 | } 43 | else { 44 | $scope.error = data.info; 45 | } 46 | }) 47 | .error(function(data) { 48 | $scope.error = "Login Error"; 49 | }); 50 | } 51 | 52 | 53 | $scope.logout = function() { 54 | 55 | if($scope.isLogin) { 56 | $http({ 57 | method : 'DELETE', 58 | url : 'api/v1/sessions', 59 | data : $.param({remember_token: $cookieStore.get('rememberToken')}), 60 | headers : {'Content-Type': 'application/x-www-form-urlencoded'} 61 | }) 62 | .success(function(data) { 63 | if(data.success) { 64 | $cookieStore.remove('rememberToken'); 65 | $cookieStore.remove('userId'); 66 | $location.path('/'); 67 | } 68 | else { 69 | $scope.error = data.info; 70 | } 71 | }) 72 | .error(function(data) { 73 | $scope.error = "Logout Error" 74 | }); 75 | } 76 | } 77 | 78 | $scope.checkLogin = function() { 79 | if($cookieStore.get('rememberToken') == null) { 80 | $scope.isLogin = false; 81 | return false; 82 | } 83 | else { 84 | $scope.isLogin = true; 85 | return true; 86 | } 87 | } 88 | 89 | $scope.submit = function() { 90 | if ($scope.form.file.$valid) { 91 | $scope.signup($scope.file); 92 | } 93 | else { 94 | $scope.error = "Invalid File!"; 95 | } 96 | } 97 | 98 | $scope.signup = function (file) { 99 | if($scope.newUser.user.password == $scope.newUser.user.password_confirmation) { 100 | $scope.newUser.user.avatar = file; 101 | delete $scope.newUser.user.password_confirmation; 102 | 103 | Upload.upload({ 104 | url: 'api/v1/users', 105 | data: $scope.newUser 106 | }) 107 | .success(function(data) { 108 | if(data.success) { 109 | $cookieStore.put('rememberToken', data.data.remember_token); 110 | $cookieStore.put('userId', data.data.user_id); 111 | $scope.isLogin = true; 112 | $location.path('/'); 113 | } 114 | else { 115 | $scope.error = data.info; 116 | } 117 | }) 118 | .error(function(data) { 119 | $scope.error = "Signup Error"; 120 | }); 121 | } 122 | else { 123 | $scope.error = "Password Does not Match!"; 124 | } 125 | } 126 | 127 | }); -------------------------------------------------------------------------------- /public/scripts/controllers/questionsController.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('QuoraClone') 3 | .controller('questionsController', function($scope, $http, $location, $cookieStore, $routeParams) { 4 | $scope.question = {}; 5 | $scope.error = null; 6 | $scope.questions = null; 7 | 8 | $scope.answer = {} 9 | $scope.answer.answer = {}; 10 | 11 | $scope.init = function(val) { 12 | if(val == "feed") { 13 | $scope.questionFeed(); 14 | } 15 | else if(val == "show") { 16 | $scope.showQuestion(); 17 | } 18 | } 19 | 20 | $scope.addQuestion = function() { 21 | 22 | topics = $scope.question.question.topics; 23 | 24 | if(topics != null) { 25 | topics = topics.split(','); 26 | temp = topics.join('~').toLowerCase(); 27 | topics = temp.split('~'); 28 | } 29 | 30 | $scope.question.question.topics = topics; 31 | $scope.question.question.votes = 0; 32 | 33 | $http({ 34 | method : 'POST', 35 | url : 'api/v1/questions', 36 | data : $scope.question, 37 | headers : {'Content-Type': 'application/json'} 38 | }) 39 | .success(function(data) { 40 | 41 | if(data.success == true) { 42 | $scope.question = {}; 43 | $location.path('/question/' + data.data.id); 44 | } 45 | else { 46 | $scope.error = data.info; 47 | } 48 | }) 49 | .error(function(data) { 50 | $scope.error = "Add Question Error"; 51 | }); 52 | } 53 | 54 | 55 | $scope.questionFeed = function() { 56 | $scope.question.question = {}; 57 | $scope.question.question.user_id = $cookieStore.get('userId'); 58 | 59 | $http({ 60 | method : 'POST', 61 | url : 'api/v1/question_feed', 62 | data : $scope.question, 63 | headers : {'Content-Type': 'application/json'} 64 | }) 65 | .success(function(data) { 66 | $scope.questions = data; 67 | if(data.length == 0) { 68 | $scope.error = "Please follow Topics to get Feed!" 69 | } 70 | }) 71 | .error(function(data) { 72 | $scope.error = "Fetch Error"; 73 | }) 74 | } 75 | 76 | $scope.showQuestion = function() { 77 | $scope.question.question = {}; 78 | $scope.question.question.id = $routeParams.id; 79 | 80 | $http({ 81 | method : 'POST', 82 | url : 'api/v1/question', 83 | data : $scope.question, 84 | headers : {'Content-Type': 'application/json'} 85 | }) 86 | .success(function(data) { 87 | $scope.question = data; 88 | console.log(data); 89 | }) 90 | .error(function(data) { 91 | $scope.error = "Question Fetch Error"; 92 | }) 93 | } 94 | 95 | $scope.submitAnswer = function() { 96 | $scope.answer.answer.question_id = $routeParams.id; 97 | $scope.answer.answer.votes = 0; 98 | console.log("Answer: " + $scope.answer); 99 | 100 | $http({ 101 | method : 'POST', 102 | url : 'api/v1/answers', 103 | data : $scope.answer, 104 | headers : {'Content-Type': 'application/json'} 105 | }) 106 | .success(function(data) { 107 | $scope.success = "Answer Addedd!"; 108 | }) 109 | .error(function(data) { 110 | $scope.error = "Cannot answer"; 111 | }) 112 | } 113 | 114 | $scope.showUser = function(id) { 115 | $location.path('/profile/' + id) 116 | } 117 | 118 | $scope.redirectToQuestion = function(id) { 119 | $location.path("/question/" + id); 120 | } 121 | 122 | $scope.redirectToTopic = function(id) { 123 | $location.path("/topics/" + id); 124 | } 125 | 126 | 127 | $scope.showComments = function(id) { 128 | console.log(id); 129 | $location.path('/' + id + '/comments') 130 | } 131 | }); -------------------------------------------------------------------------------- /public/scripts/res/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.4.9 3 | (c) 2010-2015 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(J,f,C){'use strict';function D(t,e){e=e||{};f.forEach(e,function(f,k){delete e[k]});for(var k in t)!t.hasOwnProperty(k)||"$"===k.charAt(0)&&"$"===k.charAt(1)||(e[k]=t[k]);return e}var y=f.$$minErr("$resource"),B=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;f.module("ngResource",["ng"]).provider("$resource",function(){var t=/^https?:\/\/[^\/]*/,e=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}}; 7 | this.$get=["$http","$log","$q",function(k,F,G){function w(f,g){this.template=f;this.defaults=r({},e.defaults,g);this.urlParams={}}function z(l,g,s,h){function c(a,q){var c={};q=r({},g,q);u(q,function(b,q){x(b)&&(b=b());var m;if(b&&b.charAt&&"@"==b.charAt(0)){m=a;var d=b.substr(1);if(null==d||""===d||"hasOwnProperty"===d||!B.test("."+d))throw y("badmember",d);for(var d=d.split("."),n=0,g=d.length;n 20160304093836) do 15 | 16 | create_table "answers", :force => true do |t| 17 | t.text "body" 18 | t.integer "votes" 19 | t.integer "user_id" 20 | t.integer "question_id" 21 | t.datetime "created_at", :null => false 22 | t.datetime "updated_at", :null => false 23 | end 24 | 25 | add_index "answers", ["question_id"], :name => "index_answers_on_question_id" 26 | add_index "answers", ["user_id"], :name => "index_answers_on_user_id" 27 | 28 | create_table "comments", :force => true do |t| 29 | t.text "body" 30 | t.integer "user_id" 31 | t.integer "answer_id" 32 | t.datetime "created_at", :null => false 33 | t.datetime "updated_at", :null => false 34 | end 35 | 36 | add_index "comments", ["answer_id"], :name => "index_comments_on_answer_id" 37 | add_index "comments", ["user_id"], :name => "index_comments_on_user_id" 38 | 39 | create_table "question_topics", :force => true do |t| 40 | t.integer "question_id" 41 | t.integer "topic_id" 42 | t.datetime "created_at", :null => false 43 | t.datetime "updated_at", :null => false 44 | end 45 | 46 | add_index "question_topics", ["question_id", "topic_id"], :name => "index_question_topics_on_question_id_and_topic_id", :unique => true 47 | add_index "question_topics", ["question_id"], :name => "index_question_topics_on_question_id" 48 | add_index "question_topics", ["topic_id"], :name => "index_question_topics_on_topic_id" 49 | 50 | create_table "questions", :force => true do |t| 51 | t.text "title" 52 | t.text "description" 53 | t.integer "votes" 54 | t.integer "user_id" 55 | t.datetime "created_at", :null => false 56 | t.datetime "updated_at", :null => false 57 | end 58 | 59 | add_index "questions", ["user_id"], :name => "index_questions_on_user_id" 60 | 61 | create_table "relationships", :force => true do |t| 62 | t.integer "follower_id" 63 | t.integer "followed_id" 64 | t.datetime "created_at", :null => false 65 | t.datetime "updated_at", :null => false 66 | end 67 | 68 | add_index "relationships", ["followed_id"], :name => "index_relationships_on_followed_id" 69 | add_index "relationships", ["follower_id", "followed_id"], :name => "index_relationships_on_follower_id_and_followed_id", :unique => true 70 | add_index "relationships", ["follower_id"], :name => "index_relationships_on_follower_id" 71 | 72 | create_table "topics", :force => true do |t| 73 | t.string "title" 74 | t.text "description" 75 | t.datetime "created_at", :null => false 76 | t.datetime "updated_at", :null => false 77 | end 78 | 79 | create_table "user_topics", :force => true do |t| 80 | t.integer "user_id" 81 | t.integer "topic_id" 82 | t.datetime "created_at", :null => false 83 | t.datetime "updated_at", :null => false 84 | end 85 | 86 | add_index "user_topics", ["topic_id"], :name => "index_user_topics_on_topic_id" 87 | add_index "user_topics", ["user_id", "topic_id"], :name => "index_user_topics_on_user_id_and_topic_id", :unique => true 88 | add_index "user_topics", ["user_id"], :name => "index_user_topics_on_user_id" 89 | 90 | create_table "users", :force => true do |t| 91 | t.string "first_name" 92 | t.string "last_name" 93 | t.string "email" 94 | t.datetime "created_at", :null => false 95 | t.datetime "updated_at", :null => false 96 | t.string "avatar_file_name" 97 | t.string "avatar_content_type" 98 | t.integer "avatar_file_size" 99 | t.datetime "avatar_updated_at" 100 | t.string "password_digest" 101 | t.string "remember_token" 102 | end 103 | 104 | add_index "users", ["remember_token"], :name => "index_users_on_remember_token" 105 | 106 | end 107 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (3.2.18) 5 | actionpack (= 3.2.18) 6 | mail (~> 2.5.4) 7 | actionpack (3.2.18) 8 | activemodel (= 3.2.18) 9 | activesupport (= 3.2.18) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | journey (~> 1.0.4) 13 | rack (~> 1.4.5) 14 | rack-cache (~> 1.2) 15 | rack-test (~> 0.6.1) 16 | sprockets (~> 2.2.1) 17 | activemodel (3.2.18) 18 | activesupport (= 3.2.18) 19 | builder (~> 3.0.0) 20 | activerecord (3.2.18) 21 | activemodel (= 3.2.18) 22 | activesupport (= 3.2.18) 23 | arel (~> 3.0.2) 24 | tzinfo (~> 0.3.29) 25 | activeresource (3.2.18) 26 | activemodel (= 3.2.18) 27 | activesupport (= 3.2.18) 28 | activesupport (3.2.18) 29 | i18n (~> 0.6, >= 0.6.4) 30 | multi_json (~> 1.0) 31 | arel (3.0.3) 32 | bcrypt-ruby (3.0.1) 33 | brakeman (3.2.1) 34 | erubis (~> 2.6) 35 | haml (>= 3.0, < 5.0) 36 | highline (>= 1.6.20, < 2.0) 37 | ruby2ruby (~> 2.3.0) 38 | ruby_parser (~> 3.8.1) 39 | safe_yaml (>= 1.0) 40 | sass (~> 3.0) 41 | slim (>= 1.3.6, < 4.0) 42 | terminal-table (~> 1.4) 43 | builder (3.0.4) 44 | byebug (8.2.2) 45 | choice (0.2.0) 46 | climate_control (0.0.3) 47 | activesupport (>= 3.0) 48 | cocaine (0.5.8) 49 | climate_control (>= 0.0.3, < 1.0) 50 | coffee-rails (3.2.2) 51 | coffee-script (>= 2.2.0) 52 | railties (~> 3.2.0) 53 | coffee-script (2.4.1) 54 | coffee-script-source 55 | execjs 56 | coffee-script-source (1.10.0) 57 | erubis (2.7.0) 58 | execjs (2.6.0) 59 | haml (4.0.7) 60 | tilt 61 | highline (1.7.8) 62 | hike (1.2.3) 63 | i18n (0.7.0) 64 | jbuilder (2.4.1) 65 | activesupport (>= 3.0.0, < 5.1) 66 | multi_json (~> 1.2) 67 | journey (1.0.4) 68 | jquery-rails (3.1.4) 69 | railties (>= 3.0, < 5.0) 70 | thor (>= 0.14, < 2.0) 71 | jquery-ui-rails (5.0.5) 72 | railties (>= 3.2.16) 73 | json (1.8.3) 74 | mail (2.5.4) 75 | mime-types (~> 1.16) 76 | treetop (~> 1.4.8) 77 | mime-types (1.25.1) 78 | mimemagic (0.3.0) 79 | multi_json (1.11.2) 80 | mysql (2.9.1) 81 | paperclip (4.3.5) 82 | activemodel (>= 3.2.0) 83 | activesupport (>= 3.2.0) 84 | cocaine (~> 0.5.5) 85 | mime-types 86 | mimemagic (= 0.3.0) 87 | polyglot (0.3.5) 88 | rack (1.4.7) 89 | rack-cache (1.6.1) 90 | rack (>= 0.4) 91 | rack-ssl (1.3.4) 92 | rack 93 | rack-test (0.6.3) 94 | rack (>= 1.0) 95 | rails (3.2.18) 96 | actionmailer (= 3.2.18) 97 | actionpack (= 3.2.18) 98 | activerecord (= 3.2.18) 99 | activeresource (= 3.2.18) 100 | activesupport (= 3.2.18) 101 | bundler (~> 1.0) 102 | railties (= 3.2.18) 103 | rails-erd (1.4.6) 104 | activerecord (>= 3.2) 105 | activesupport (>= 3.2) 106 | choice (~> 0.2.0) 107 | ruby-graphviz (~> 1.2) 108 | railties (3.2.18) 109 | actionpack (= 3.2.18) 110 | activesupport (= 3.2.18) 111 | rack-ssl (~> 1.3.2) 112 | rake (>= 0.8.7) 113 | rdoc (~> 3.4) 114 | thor (>= 0.14.6, < 2.0) 115 | rake (10.5.0) 116 | rdoc (3.12.2) 117 | json (~> 1.4) 118 | ruby-graphviz (1.2.2) 119 | ruby2ruby (2.3.0) 120 | ruby_parser (~> 3.1) 121 | sexp_processor (~> 4.0) 122 | ruby_parser (3.8.1) 123 | sexp_processor (~> 4.1) 124 | safe_yaml (1.0.4) 125 | sass (3.4.21) 126 | sass-rails (3.2.6) 127 | railties (~> 3.2.0) 128 | sass (>= 3.1.10) 129 | tilt (~> 1.3) 130 | sexp_processor (4.7.0) 131 | slim (3.0.6) 132 | temple (~> 0.7.3) 133 | tilt (>= 1.3.3, < 2.1) 134 | sprockets (2.2.3) 135 | hike (~> 1.2) 136 | multi_json (~> 1.0) 137 | rack (~> 1.0) 138 | tilt (~> 1.1, != 1.3.0) 139 | strong_parameters (0.2.3) 140 | actionpack (~> 3.0) 141 | activemodel (~> 3.0) 142 | activesupport (~> 3.0) 143 | railties (~> 3.0) 144 | temple (0.7.6) 145 | terminal-table (1.5.2) 146 | thor (0.19.1) 147 | tilt (1.4.1) 148 | treetop (1.4.15) 149 | polyglot 150 | polyglot (>= 0.3.1) 151 | tzinfo (0.3.46) 152 | uglifier (2.7.2) 153 | execjs (>= 0.3.0) 154 | json (>= 1.8.0) 155 | 156 | PLATFORMS 157 | ruby 158 | 159 | DEPENDENCIES 160 | bcrypt-ruby (= 3.0.1) 161 | brakeman 162 | byebug 163 | coffee-rails (~> 3.2.1) 164 | jbuilder 165 | jquery-rails 166 | jquery-ui-rails 167 | mysql 168 | paperclip (~> 4.1) 169 | rails (= 3.2.18) 170 | rails-erd 171 | sass-rails (~> 3.2.3) 172 | strong_parameters 173 | uglifier (>= 1.0.3) 174 | -------------------------------------------------------------------------------- /public/scripts/controllers/usersController.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('QuoraClone') 3 | .controller('usersController', function($scope, $http, $location, $cookieStore, $routeParams) { 4 | 5 | $scope.firstName = ""; 6 | $scope.lastName = ""; 7 | $scope.followers = null; 8 | $scope.followings = null; 9 | $scope.avatarUrl = ""; 10 | $scope.topics = 0; 11 | $scope.user = {}; 12 | $scope.user.user = {}; 13 | $scope.question = {}; 14 | $scope.question.question = {}; 15 | $scope.question.question = {}; 16 | $scope.followText = "Follow"; 17 | $scope.error = null; 18 | $scope.success = null; 19 | $scope.success1 = null; 20 | $scope.success2 = null; 21 | $scope.click = true; 22 | 23 | $scope.init = function() { 24 | $scope.curUserId = $cookieStore.get('userId'); 25 | $scope.userId = $routeParams.id; 26 | $scope.user.user.id = $scope.userId; 27 | $scope.question.question.user_id = $scope.userId; 28 | $scope.showProfile(); 29 | $scope.showQuestions(); 30 | $scope.showAnswers(); 31 | } 32 | 33 | $scope.showProfile = function() { 34 | 35 | $http({ 36 | method : 'POST', 37 | url : 'api/v1/show_user', 38 | data : $scope.user, 39 | headers : {'Content-Type': 'application/json'} 40 | }) 41 | .success(function(data) { 42 | if(data.success) { 43 | $scope.user = data; 44 | $scope.firstName = data.first_name; 45 | $scope.lastName = data.last_name; 46 | $scope.avatarUrl = data.avatar; 47 | $scope.followers = data.followers; 48 | $scope.followings = data.followed_users; 49 | $scope.email = data.email; 50 | $scope.topics = data.topics; 51 | $scope.follow = $scope.checkFollowers($scope.user.id); 52 | } 53 | else { 54 | $scope.error = "Profile Fetch Error"; 55 | } 56 | }) 57 | .error(function(data) { 58 | $scope.error = "Profile Fetch Error"; 59 | }); 60 | } 61 | 62 | $scope.showQuestions = function() { 63 | $http({ 64 | method : 'POST', 65 | url : 'api/v1/question_user', 66 | data : $scope.question, 67 | headers : {'Content-Type': 'application/json'} 68 | }) 69 | .success(function(data) { 70 | $scope.questions = data; 71 | if($scope.questions.length == 0) { 72 | $scope.success1 = "No Questions asked by " + $scope.firstName; 73 | } 74 | 75 | }) 76 | .error(function(data) { 77 | $scope.error = "Question Fetch Error"; 78 | }) 79 | } 80 | 81 | $scope.showAnswers = function() { 82 | $scope.user = {}; 83 | $scope.user.user = {}; 84 | $scope.user.user.id = $scope.userId; 85 | $http({ 86 | method : 'POST', 87 | url : 'api/v1/answer_user', 88 | data : $scope.user, 89 | headers : {'Content-Type': 'application/json'} 90 | }) 91 | .success(function(data) { 92 | $scope.answers = data; 93 | if($scope.answers.length == 0) { 94 | $scope.success2 = "No Questions answered by "+ $scope.firstName; 95 | } 96 | 97 | }) 98 | .error(function(data) { 99 | $scope.error = "Answers Fetch Error"; 100 | }) 101 | } 102 | 103 | $scope.followUser = function(id) { 104 | $scope.relationships = {}; 105 | $scope.relationships.relationships = {}; 106 | $scope.relationships.relationships.followed_id = id; 107 | 108 | $http({ 109 | method : 'POST', 110 | url : 'api/v1/relationships', 111 | data : $scope.relationships, 112 | headers : {'Content-Type': 'application/json'} 113 | }) 114 | .success(function(data) { 115 | $scope.success = "Now following: " + $scope.firstName; 116 | }) 117 | .error(function(data) { 118 | $scope.error = "Cannot follow!"; 119 | }) 120 | 121 | } 122 | 123 | $scope.unFollowUser = function(id) { 124 | 125 | $http({ 126 | method : 'DELETE', 127 | url : 'api/v1/relationships?id=' + id, 128 | headers : {'Content-Type': 'application/x-www-form-urlencoded'} 129 | }) 130 | .success(function(data) { 131 | $scope.success = "Unfollowed: " + $scope.firstName; 132 | }) 133 | .error(function(data) { 134 | $scope.error = "Cannot Unfollow!"; 135 | }) 136 | 137 | } 138 | 139 | $scope.checkFollowers = function() { 140 | console.log("Followers: "); 141 | console.log($scope.user.followers); 142 | flag = true; 143 | for(var i = 0; i < $scope.user.followers.length; i++) { 144 | if($scope.user.followers[i].id == $scope.curUserId) { 145 | flag = false; 146 | break; 147 | } 148 | } 149 | if(flag) { 150 | return false; 151 | } 152 | else { 153 | return true; 154 | } 155 | } 156 | 157 | $scope.redirectToQuestion = function(id) { 158 | $location.path("/question/" + id); 159 | } 160 | 161 | $scope.redirectToTopic = function(id) { 162 | $location.path("/topics/" + id); 163 | } 164 | 165 | $scope.showUser = function(id) { 166 | $location.path('/profile/' + id) 167 | } 168 | 169 | }); -------------------------------------------------------------------------------- /public/partials/profile.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 24 | 25 | 26 | 45 | 46 | 47 | 48 | 67 | 68 |
69 | × 70 | Error! {{error}} 71 |
72 | 73 |
74 | × 75 | {{success1}} 76 |
77 | 78 | 79 |
80 | × 81 | {{success2}} 82 |
83 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 |

92 |
{{firstName + " " + lastName}}
93 |

94 | 95 | 111 | 112 |
113 |
114 | 115 |
116 | 117 |
118 | 119 |
120 |
121 | 122 |
123 | 124 |
125 | 126 | 130 | 131 | 132 |
133 |

Questions asked by {{firstName}}

134 |
135 |

{{question.title}}

136 |

Description: {{question.description}}

137 |

Answers: {{question.answers.length}}

138 |

Topics: {{question.topics.length}} 139 |

Votes: {{question.votes}} 140 |

Asked On: {{question.updated_at | date: 'medium'}}

141 |
142 |
143 | 144 |
145 |

Questions answered by {{firstName}}

146 |
147 |

{{answer.question.title}}

148 |

Answer: {{answer.body}}

149 |
150 |
151 |
152 | 153 |
154 | 155 |
-------------------------------------------------------------------------------- /public/scripts/res/ng-file-upload-shim.min.js: -------------------------------------------------------------------------------- 1 | /*! 12.0.1 */ 2 | !function(){function a(a,b){window.XMLHttpRequest.prototype[a]=b(window.XMLHttpRequest.prototype[a])}function b(a,b,c){try{Object.defineProperty(a,b,{get:c})}catch(d){}}if(window.FileAPI||(window.FileAPI={}),!window.XMLHttpRequest)throw"AJAX is not supported. XMLHttpRequest is not defined.";if(FileAPI.shouldLoad=!window.FormData||FileAPI.forceLoad,FileAPI.shouldLoad){var c=function(a){if(!a.__listeners){a.upload||(a.upload={}),a.__listeners=[];var b=a.upload.addEventListener;a.upload.addEventListener=function(c,d){a.__listeners[c]=d,b&&b.apply(this,arguments)}}};a("open",function(a){return function(b,d,e){c(this),this.__url=d;try{a.apply(this,[b,d,e])}catch(f){f.message.indexOf("Access is denied")>-1&&(this.__origError=f,a.apply(this,[b,"_fix_for_ie_crossdomain__",e]))}}}),a("getResponseHeader",function(a){return function(b){return this.__fileApiXHR&&this.__fileApiXHR.getResponseHeader?this.__fileApiXHR.getResponseHeader(b):null==a?null:a.apply(this,[b])}}),a("getAllResponseHeaders",function(a){return function(){return this.__fileApiXHR&&this.__fileApiXHR.getAllResponseHeaders?this.__fileApiXHR.getAllResponseHeaders():null==a?null:a.apply(this)}}),a("abort",function(a){return function(){return this.__fileApiXHR&&this.__fileApiXHR.abort?this.__fileApiXHR.abort():null==a?null:a.apply(this)}}),a("setRequestHeader",function(a){return function(b,d){if("__setXHR_"===b){c(this);var e=d(this);e instanceof Function&&e(this)}else this.__requestHeaders=this.__requestHeaders||{},this.__requestHeaders[b]=d,a.apply(this,arguments)}}),a("send",function(a){return function(){var c=this;if(arguments[0]&&arguments[0].__isFileAPIShim){var d=arguments[0],e={url:c.__url,jsonp:!1,cache:!0,complete:function(a,d){a&&angular.isString(a)&&-1!==a.indexOf("#2174")&&(a=null),c.__completed=!0,!a&&c.__listeners.load&&c.__listeners.load({type:"load",loaded:c.__loaded,total:c.__total,target:c,lengthComputable:!0}),!a&&c.__listeners.loadend&&c.__listeners.loadend({type:"loadend",loaded:c.__loaded,total:c.__total,target:c,lengthComputable:!0}),"abort"===a&&c.__listeners.abort&&c.__listeners.abort({type:"abort",loaded:c.__loaded,total:c.__total,target:c,lengthComputable:!0}),void 0!==d.status&&b(c,"status",function(){return 0===d.status&&a&&"abort"!==a?500:d.status}),void 0!==d.statusText&&b(c,"statusText",function(){return d.statusText}),b(c,"readyState",function(){return 4}),void 0!==d.response&&b(c,"response",function(){return d.response});var e=d.responseText||(a&&0===d.status&&"abort"!==a?a:void 0);b(c,"responseText",function(){return e}),b(c,"response",function(){return e}),a&&b(c,"err",function(){return a}),c.__fileApiXHR=d,c.onreadystatechange&&c.onreadystatechange(),c.onload&&c.onload()},progress:function(a){if(a.target=c,c.__listeners.progress&&c.__listeners.progress(a),c.__total=a.total,c.__loaded=a.loaded,a.total===a.loaded){var b=this;setTimeout(function(){c.__completed||(c.getAllResponseHeaders=function(){},b.complete(null,{status:204,statusText:"No Content"}))},FileAPI.noContentTimeout||1e4)}},headers:c.__requestHeaders};e.data={},e.files={};for(var f=0;f-1){e=h.substring(0,g+1);break}null==FileAPI.staticPath&&(FileAPI.staticPath=e),i.setAttribute("src",d||e+"FileAPI.min.js"),document.getElementsByTagName("head")[0].appendChild(i)}FileAPI.ngfFixIE=function(d,e,f){if(!b())throw'Adode Flash Player need to be installed. To check ahead use "FileAPI.hasFlash"';var g=function(){var b=e.parent();d.attr("disabled")?b&&b.removeClass("js-fileapi-wrapper"):(e.attr("__ngf_flash_")||(e.unbind("change"),e.unbind("click"),e.bind("change",function(a){h.apply(this,[a]),f.apply(this,[a])}),e.attr("__ngf_flash_","true")),b.addClass("js-fileapi-wrapper"),a(d)||(b.css("position","absolute").css("top",c(d[0]).top+"px").css("left",c(d[0]).left+"px").css("width",d[0].offsetWidth+"px").css("height",d[0].offsetHeight+"px").css("filter","alpha(opacity=0)").css("display",d.css("display")).css("overflow","hidden").css("z-index","900000").css("visibility","visible"),e.css("width",d[0].offsetWidth+"px").css("height",d[0].offsetHeight+"px").css("position","absolute").css("top","0px").css("left","0px")))};d.bind("mouseenter",g);var h=function(a){for(var b=FileAPI.getFiles(a),c=0;c= reqsTotal) { 119 | setComplete(); 120 | } else { 121 | cfpLoadingBar.set(reqsCompleted / reqsTotal); 122 | } 123 | } 124 | return response; 125 | }, 126 | 127 | 'responseError': function(rejection) { 128 | if (!rejection || !rejection.config) { 129 | $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); 130 | return $q.reject(rejection); 131 | } 132 | 133 | if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) { 134 | reqsCompleted++; 135 | $rootScope.$broadcast('cfpLoadingBar:loaded', {url: rejection.config.url, result: rejection}); 136 | if (reqsCompleted >= reqsTotal) { 137 | setComplete(); 138 | } else { 139 | cfpLoadingBar.set(reqsCompleted / reqsTotal); 140 | } 141 | } 142 | return $q.reject(rejection); 143 | } 144 | }; 145 | }]; 146 | 147 | $httpProvider.interceptors.push(interceptor); 148 | }]); 149 | 150 | 151 | /** 152 | * Loading Bar 153 | * 154 | * This service handles adding and removing the actual element in the DOM. 155 | * Generally, best practices for DOM manipulation is to take place in a 156 | * directive, but because the element itself is injected in the DOM only upon 157 | * XHR requests, and it's likely needed on every view, the best option is to 158 | * use a service. 159 | */ 160 | angular.module('cfp.loadingBar', []) 161 | .provider('cfpLoadingBar', function() { 162 | 163 | this.autoIncrement = true; 164 | this.includeSpinner = true; 165 | this.includeBar = true; 166 | this.latencyThreshold = 100; 167 | this.startSize = 0.02; 168 | this.parentSelector = 'body'; 169 | this.spinnerTemplate = '
'; 170 | this.loadingBarTemplate = '
'; 171 | 172 | this.$get = ['$injector', '$document', '$timeout', '$rootScope', function ($injector, $document, $timeout, $rootScope) { 173 | var $animate; 174 | var $parentSelector = this.parentSelector, 175 | loadingBarContainer = angular.element(this.loadingBarTemplate), 176 | loadingBar = loadingBarContainer.find('div').eq(0), 177 | spinner = angular.element(this.spinnerTemplate); 178 | 179 | var incTimeout, 180 | completeTimeout, 181 | started = false, 182 | status = 0; 183 | 184 | var autoIncrement = this.autoIncrement; 185 | var includeSpinner = this.includeSpinner; 186 | var includeBar = this.includeBar; 187 | var startSize = this.startSize; 188 | 189 | /** 190 | * Inserts the loading bar element into the dom, and sets it to 2% 191 | */ 192 | function _start() { 193 | if (!$animate) { 194 | $animate = $injector.get('$animate'); 195 | } 196 | 197 | var $parent = $document.find($parentSelector).eq(0); 198 | $timeout.cancel(completeTimeout); 199 | 200 | // do not continually broadcast the started event: 201 | if (started) { 202 | return; 203 | } 204 | 205 | $rootScope.$broadcast('cfpLoadingBar:started'); 206 | started = true; 207 | 208 | var $children = $parent.children(); 209 | 210 | if (includeBar) { 211 | 212 | $animate.enter(loadingBarContainer, $parent, $children.length > 0 ? $children[$children.length - 1] : null); 213 | } 214 | 215 | if (includeSpinner) { 216 | $animate.enter(spinner, $parent, $children.length > 0 ? $children[$children.length - 1] : null); 217 | } 218 | 219 | _set(startSize); 220 | } 221 | 222 | /** 223 | * Set the loading bar's width to a certain percent. 224 | * 225 | * @param n any value between 0 and 1 226 | */ 227 | function _set(n) { 228 | if (!started) { 229 | return; 230 | } 231 | var pct = (n * 100) + '%'; 232 | loadingBar.css('width', pct); 233 | status = n; 234 | 235 | // increment loadingbar to give the illusion that there is always 236 | // progress but make sure to cancel the previous timeouts so we don't 237 | // have multiple incs running at the same time. 238 | if (autoIncrement) { 239 | $timeout.cancel(incTimeout); 240 | incTimeout = $timeout(function() { 241 | _inc(); 242 | }, 250); 243 | } 244 | } 245 | 246 | /** 247 | * Increments the loading bar by a random amount 248 | * but slows down as it progresses 249 | */ 250 | function _inc() { 251 | if (_status() >= 1) { 252 | return; 253 | } 254 | 255 | var rnd = 0; 256 | 257 | // TODO: do this mathmatically instead of through conditions 258 | 259 | var stat = _status(); 260 | if (stat >= 0 && stat < 0.25) { 261 | // Start out between 3 - 6% increments 262 | rnd = (Math.random() * (5 - 3 + 1) + 3) / 100; 263 | } else if (stat >= 0.25 && stat < 0.65) { 264 | // increment between 0 - 3% 265 | rnd = (Math.random() * 3) / 100; 266 | } else if (stat >= 0.65 && stat < 0.9) { 267 | // increment between 0 - 2% 268 | rnd = (Math.random() * 2) / 100; 269 | } else if (stat >= 0.9 && stat < 0.99) { 270 | // finally, increment it .5 % 271 | rnd = 0.005; 272 | } else { 273 | // after 99%, don't increment: 274 | rnd = 0; 275 | } 276 | 277 | var pct = _status() + rnd; 278 | _set(pct); 279 | } 280 | 281 | function _status() { 282 | return status; 283 | } 284 | 285 | function _completeAnimation() { 286 | status = 0; 287 | started = false; 288 | } 289 | 290 | function _complete() { 291 | if (!$animate) { 292 | $animate = $injector.get('$animate'); 293 | } 294 | 295 | $rootScope.$broadcast('cfpLoadingBar:completed'); 296 | _set(1); 297 | 298 | $timeout.cancel(completeTimeout); 299 | 300 | // Attempt to aggregate any start/complete calls within 500ms: 301 | completeTimeout = $timeout(function() { 302 | var promise = $animate.leave(loadingBarContainer, _completeAnimation); 303 | if (promise && promise.then) { 304 | promise.then(_completeAnimation); 305 | } 306 | $animate.leave(spinner); 307 | }, 500); 308 | } 309 | 310 | return { 311 | start : _start, 312 | set : _set, 313 | status : _status, 314 | inc : _inc, 315 | complete : _complete, 316 | autoIncrement : this.autoIncrement, 317 | includeSpinner : this.includeSpinner, 318 | latencyThreshold : this.latencyThreshold, 319 | parentSelector : this.parentSelector, 320 | startSize : this.startSize 321 | }; 322 | 323 | 324 | }]; // 325 | }); // wtf javascript. srsly 326 | })(); // 327 | -------------------------------------------------------------------------------- /public/scripts/res/angular-ui-router.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * State-based routing for AngularJS 3 | * @version v0.2.18 4 | * @link http://angular-ui.github.com/ 5 | * @license MIT License, http://www.opensource.org/licenses/MIT 6 | */ 7 | "undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return R(new(R(function(){},{prototype:a})),b)}function e(a){return Q(arguments,function(b){b!==a&&Q(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a){if(Object.keys)return Object.keys(a);var b=[];return Q(a,function(a,c){b.push(c)}),b}function h(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function i(a,b,c,d){var e,i=f(c,d),j={},k=[];for(var l in i)if(i[l]&&i[l].params&&(e=g(i[l].params),e.length))for(var m in e)h(k,e[m])>=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return R({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(s[c]=d,N(a))q.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);Q(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),q.push(c,a,e)}r.pop(),s[c]=f}}function o(a){return O(a)&&a.then&&a.$$promises}if(!O(i))throw new Error("'invocables' must be an object");var p=g(i||{}),q=[],r=[],s={};return Q(i,n),i=r=s=null,function(d,f,g){function h(){--u||(v||e(t,f.$$values),r.$$values=t,r.$$promises=r.$$promises||!0,delete r.$$inheritedValues,n.resolve(t))}function i(a){r.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!L(r.$$failure))try{l.resolve(b.invoke(e,g,t)),l.promise.then(function(a){t[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;Q(f,function(a){s.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,s[a].then(function(b){t[a]=b,--m||k()},j))}),m||k(),s[c]=l.promise}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!O(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=l;var n=a.defer(),r=n.promise,s=r.$$promises={},t=R({},d),u=1+q.length/3,v=!1;if(L(f.$$failure))return i(f.$$failure),r;f.$$inheritedValues&&e(t,m(f.$$inheritedValues,p)),R(s,f.$$promises),f.$$values?(v=e(t,m(f.$$values,p)),r.$$inheritedValues=m(f.$$values,p),h()):(f.$$inheritedValues&&(r.$$inheritedValues=m(f.$$inheritedValues,p)),f.then(h,i));for(var w=0,x=q.length;x>w;w+=3)d.hasOwnProperty(q[w])?h():j(q[w],q[w+1],q[w+2]);return r}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function q(a,b,c){this.fromConfig=function(a,b,c){return L(a.template)?this.fromString(a.template,b):L(a.templateUrl)?this.fromUrl(a.templateUrl,b):L(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return M(a)?a(b):a},this.fromUrl=function(c,d){return M(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b,headers:{Accept:"text/html"}}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function r(a,b,e){function f(b,c,d,e){if(q.push(b),o[b])return o[b];if(!/^\w+([-.]+\w+)*(?:\[\])?$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(p[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");return p[b]=new U.Param(b,c,d,e),p[b]}function g(a,b,c,d){var e=["",""],f=a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!b)return f;switch(c){case!1:e=["(",")"+(d?"?":"")];break;case!0:f=f.replace(/\/$/,""),e=["(?:/(",")|/)?"];break;default:e=["("+c+"|",")?"]}return f+e[0]+b+e[1]}function h(e,f){var g,h,i,j,k;return g=e[2]||e[3],k=b.params[g],i=a.substring(m,e.index),h=f?e[4]:e[4]||("*"==e[1]?".*":null),h&&(j=U.type(h)||d(U.type("string"),{pattern:new RegExp(h,b.caseInsensitive?"i":c)})),{id:g,regexp:h,segment:i,type:j,cfg:k}}b=R({params:{}},O(b)?b:{});var i,j=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,k=/([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l="^",m=0,n=this.segments=[],o=e?e.params:{},p=this.params=e?e.params.$$new():new U.ParamSet,q=[];this.source=a;for(var r,s,t;(i=j.exec(a))&&(r=h(i,!1),!(r.segment.indexOf("?")>=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash,s.isOptional),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(b.strict===!1?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function s(a){R(this,a)}function t(){function a(a){return null!=a?a.toString().replace(/~/g,"~~").replace(/\//g,"~2F"):a}function e(a){return null!=a?a.toString().replace(/~2F/g,"/").replace(/~~/g,"~"):a}function f(){return{strict:p,caseInsensitive:m}}function i(a){return M(a)||P(a)&&M(a[a.length-1])}function j(){for(;w.length;){var a=w.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(u[a.name],l.invoke(a.def))}}function k(a){R(this,a||{})}U=this;var l,m=!1,p=!0,q=!1,u={},v=!0,w=[],x={string:{encode:a,decode:e,is:function(a){return null==a||!L(a)||"string"==typeof a},pattern:/[^\/]*/},"int":{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return L(a)&&this.decode(a.toString())===a},pattern:/\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return a===!0||a===!1},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^\/]*/},any:{encode:b.identity,decode:b.identity,equals:b.equals,pattern:/.*/}};t.$$getDefaultValue=function(a){if(!i(a.value))return a.value;if(!l)throw new Error("Injectable functions cannot be called at configuration time");return l.invoke(a.value)},this.caseInsensitive=function(a){return L(a)&&(m=a),m},this.strictMode=function(a){return L(a)&&(p=a),p},this.defaultSquashPolicy=function(a){if(!L(a))return q;if(a!==!0&&a!==!1&&!N(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return q=a,a},this.compile=function(a,b){return new r(a,R(f(),b))},this.isMatcher=function(a){if(!O(a))return!1;var b=!0;return Q(r.prototype,function(c,d){M(c)&&(b=b&&L(a[d])&&M(a[d]))}),b},this.type=function(a,b,c){if(!L(b))return u[a];if(u.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return u[a]=new s(R({name:a},b)),c&&(w.push({name:a,def:c}),v||j()),this},Q(x,function(a,b){u[b]=new s(R({name:b},a))}),u=d(u,{}),this.$get=["$injector",function(a){return l=a,v=!1,j(),Q(x,function(a,b){u[b]||(u[b]=new s(a))}),this}],this.Param=function(a,d,e,f){function j(a){var b=O(a)?g(a):[],c=-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array");return c&&(a={value:a}),a.$$fn=i(a.value)?a.value:function(){return a.value},a}function k(c,d,e){if(c.type&&d)throw new Error("Param '"+a+"' has two type configurations.");return d?d:c.type?b.isString(c.type)?u[c.type]:c.type instanceof s?c.type:new s(c.type):"config"===e?u.any:u.string}function m(){var b={array:"search"===f?"auto":!1},c=a.match(/\[\]$/)?{array:!0}:{};return R(b,c,e).array}function p(a,b){var c=a.squash;if(!b||c===!1)return!1;if(!L(c)||null==c)return q;if(c===!0||N(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function r(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=P(a.replace)?a.replace:[],N(e)&&f.push({from:e,to:c}),g=o(f,function(a){return a.from}),n(i,function(a){return-1===h(g,a.from)}).concat(f)}function t(){if(!l)throw new Error("Injectable functions cannot be called at configuration time");var a=l.invoke(e.$$fn);if(null!==a&&a!==c&&!x.type.is(a))throw new Error("Default value ("+a+") for parameter '"+x.id+"' is not an instance of Type ("+x.type.name+")");return a}function v(a){function b(a){return function(b){return b.from===a}}function c(a){var c=o(n(x.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),L(a)?x.type.$normalize(a):t()}function w(){return"{Param:"+a+" "+d+" squash: '"+A+"' optional: "+z+"}"}var x=this;e=j(e),d=k(e,d,f);var y=m();d=y?d.$asArray(y,"search"===f):d,"string"!==d.name||y||"path"!==f||e.value!==c||(e.value="");var z=e.value!==c,A=p(e,z),B=r(e,y,z,A);R(this,{id:a,type:d,location:f,array:y,squash:A,replace:B,isOptional:z,value:v,dynamic:c,config:e,toString:w})},k.prototype={$$new:function(){return d(this,R(new k,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(k.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),Q(b,function(b){Q(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return Q(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return Q(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var d,e,f,g,h,i=this.$$keys();for(d=0;de;e++)if(b(j[e]))return;k&&b(k)}}function o(){return i=i||e.$on("$locationChangeSuccess",n)}var p,q=g.baseHref(),r=d.url();return l||o(),{sync:function(){n()},listen:function(){return o()},update:function(a){return a?void(r=d.url()):void(d.url()!==r&&(d.url(r),d.replace()))},push:function(a,b,e){var f=a.format(b||{});null!==f&&b&&b["#"]&&(f+="#"+b["#"]),d.url(f),p=e&&e.$$avoidResync?d.url():c,e&&e.replace&&d.replace()},href:function(c,e,f){if(!c.validates(e))return null;var g=a.html5Mode();b.isObject(g)&&(g=g.enabled),g=g&&h.history;var i=c.format(e);if(f=f||{},g||null===i||(i="#"+a.hashPrefix()+i),null!==i&&e&&e["#"]&&(i+="#"+e["#"]),i=m(i,g,f.absolute),!f.absolute||!i)return i;var j=!g&&i?"/":"",k=d.port();return k=80===k||443===k?"":":"+k,[d.protocol(),"://",d.host(),k,j,i].join("")}}}var i,j=[],k=null,l=!1;this.rule=function(a){if(!M(a))throw new Error("'rule' must be a function");return j.push(a),this},this.otherwise=function(a){if(N(a)){var b=a;a=function(){return b}}else if(!M(a))throw new Error("'rule' must be a function");return k=a,this},this.when=function(a,b){var c,h=N(b);if(N(a)&&(a=d.compile(a)),!h&&!M(b)&&!P(b))throw new Error("invalid 'handler' in when()");var i={matcher:function(a,b){return h&&(c=d.compile(b),b=["$match",function(a){return c.format(a)}]),R(function(c,d){return g(c,b,a.exec(d.path(),d.search()))},{prefix:N(a.prefix)?a.prefix:""})},regex:function(a,b){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(c=b,b=["$match",function(a){return f(c,a)}]),R(function(c,d){return g(c,b,a.exec(d.path()))},{prefix:e(a)})}},j={matcher:d.isMatcher(a),regex:a instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](a,b));throw new Error("invalid 'what' in when()")},this.deferIntercept=function(a){a===c&&(a=!0),l=a},this.$get=h,h.$inject=["$location","$rootScope","$injector","$browser","$sniffer"]}function v(a,e){function f(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function m(a,b){if(!a)return c;var d=N(a),e=d?a:a.name,g=f(e);if(g){if(!b)throw new Error("No reference point given for path '"+e+"'");b=m(b);for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var l=z[e];return!l||!d&&(d||l!==a&&l.self!==a)?c:l}function n(a,b){A[a]||(A[a]=[]),A[a].push(b)}function p(a){for(var b=A[a]||[];b.length;)q(b.shift())}function q(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!N(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(z.hasOwnProperty(c))throw new Error("State '"+c+"' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):N(b.parent)?b.parent:O(b.parent)&&N(b.parent.name)?b.parent.name:"";if(e&&!z[e])return n(e,b.self);for(var f in C)M(C[f])&&(b[f]=C[f](b,C.$delegates[f]));return z[c]=b,!b[B]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){y.$current.navigable==b&&j(a,c)||y.transitionTo(b,a,{inherit:!0,location:!1})}]),p(c),b}function r(a){return a.indexOf("*")>-1}function s(a){for(var b=a.split("."),c=y.$current.name.split("."),d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return"**"===b[0]&&(c=c.slice(h(c,b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(h(c,b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length?!1:c.join("")===b.join("")}function t(a,b){return N(a)&&!L(b)?C[a]:M(b)&&N(a)?(C[a]&&!C.$delegates[a]&&(C.$delegates[a]=C[a]),C[a]=b,this):this}function u(a,b){return O(a)?b=a:b.name=a,q(b),this}function v(a,e,f,h,l,n,p,q,t){function u(b,c,d,f){var g=a.$broadcast("$stateNotFound",b,c,d);if(g.defaultPrevented)return p.update(),D;if(!g.retry)return null;if(f.$retry)return p.update(),E;var h=y.transition=e.when(g.retry);return h.then(function(){return h!==y.transition?A:(b.options.$retry=!0,y.transitionTo(b.to,b.toParams,b.options))},function(){return D}),p.update(),h}function v(a,c,d,g,i,j){function m(){var c=[];return Q(a.views,function(d,e){var g=d.resolve&&d.resolve!==a.resolve?d.resolve:{};g.$template=[function(){return f.load(e,{view:d,locals:i.globals,params:n,notify:j.notify})||""}],c.push(l.resolve(g,i.globals,i.resolve,a).then(function(c){if(M(d.controllerProvider)||P(d.controllerProvider)){var f=b.extend({},g,i.globals);c.$$controller=h.invoke(d.controllerProvider,null,f)}else c.$$controller=d.controller;c.$$state=a,c.$$controllerAs=d.controllerAs,i[e]=c}))}),e.all(c).then(function(){return i.globals})}var n=d?c:k(a.params.$$keys(),c),o={$stateParams:n};i.resolve=l.resolve(a.resolve,o,i.resolve,a);var p=[i.resolve.then(function(a){i.globals=a})];return g&&p.push(g),e.all(p).then(m).then(function(a){return i})}var A=e.reject(new Error("transition superseded")),C=e.reject(new Error("transition prevented")),D=e.reject(new Error("transition aborted")),E=e.reject(new Error("transition failed"));return x.locals={resolve:null,globals:{$stateParams:{}}},y={params:{},current:x.self,$current:x,transition:null},y.reload=function(a){return y.transitionTo(y.current,n,{reload:a||!0,inherit:!1,notify:!0})},y.go=function(a,b,c){return y.transitionTo(a,b,R({inherit:!0,relative:y.$current},c))},y.transitionTo=function(b,c,f){c=c||{},f=R({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,j=y.$current,l=y.params,o=j.path,q=m(b,f.relative),r=c["#"];if(!L(q)){var s={to:b,toParams:c,options:f},t=u(s,j.self,l,f);if(t)return t;if(b=s.to,c=s.toParams,f=s.options,q=m(b,f.relative),!L(q)){if(!f.relative)throw new Error("No such state '"+b+"'");throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'")}}if(q[B])throw new Error("Cannot transition to abstract state '"+b+"'");if(f.inherit&&(c=i(n,c||{},y.$current,q)),!q.params.$$validates(c))return E;c=q.params.$$values(c),b=q;var z=b.path,D=0,F=z[D],G=x.locals,H=[];if(f.reload){if(N(f.reload)||O(f.reload)){if(O(f.reload)&&!f.reload.name)throw new Error("Invalid reload state object");var I=f.reload===!0?o[0]:m(f.reload);if(f.reload&&!I)throw new Error("No such reload state '"+(N(f.reload)?f.reload:f.reload.name)+"'");for(;F&&F===o[D]&&F!==I;)G=H[D]=F.locals,D++,F=z[D]}}else for(;F&&F===o[D]&&F.ownParams.$$equals(c,l);)G=H[D]=F.locals,D++,F=z[D];if(w(b,c,j,l,G,f))return r&&(c["#"]=r),y.params=c,S(y.params,n),S(k(b.params.$$keys(),n),b.locals.globals.$stateParams),f.location&&b.navigable&&b.navigable.url&&(p.push(b.navigable.url,c,{$$avoidResync:!0,replace:"replace"===f.location}),p.update(!0)),y.transition=null,e.when(y.current);if(c=k(b.params.$$keys(),c||{}),r&&(c["#"]=r),f.notify&&a.$broadcast("$stateChangeStart",b.self,c,j.self,l,f).defaultPrevented)return a.$broadcast("$stateChangeCancel",b.self,c,j.self,l),null==y.transition&&p.update(),C;for(var J=e.when(G),K=D;K=D;d--)g=o[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=D;d=4?!!j.enabled(a):1===V&&W>=2?!!j.enabled():!!i}var e={enter:function(a,b,c){b.after(a),c()},leave:function(a,b){a.remove(),b()}};if(a.noanimation)return e;if(j)return{enter:function(a,c,f){d(a)?b.version.minor>2?j.enter(a,null,c).then(f):j.enter(a,null,c,f):e.enter(a,c,f)},leave:function(a,c){d(a)?b.version.minor>2?j.leave(a).then(c):j.leave(a,c):e.leave(a,c)}};if(i){var f=i&&i(c,a);return{enter:function(a,b,c){f.enter(a,null,b),c()},leave:function(a,b){f.leave(a),b()}}}return e}var h=f(),i=h("$animator"),j=h("$animate"),k={restrict:"ECA",terminal:!0,priority:400,transclude:"element",compile:function(c,f,h){return function(c,f,i){function j(){function a(){b&&b.remove(),c&&c.$destroy()}var b=l,c=n;c&&(c._willBeDestroyed=!0),m?(r.leave(m,function(){a(),l=null}),l=m):(a(),l=null),m=null,n=null}function k(g){var k,l=A(c,i,f,e),s=l&&a.$current&&a.$current.locals[l];if((g||s!==o)&&!c._willBeDestroyed){k=c.$new(),o=a.$current.locals[l],k.$emit("$viewContentLoading",l);var t=h(k,function(a){r.enter(a,f,function(){n&&n.$emit("$viewContentAnimationEnded"),(b.isDefined(q)&&!q||c.$eval(q))&&d(a)}),j()});m=t,n=k,n.$emit("$viewContentLoaded",l),n.$eval(p)}}var l,m,n,o,p=i.onload||"",q=i.autoscroll,r=g(i,c);c.$on("$stateChangeSuccess",function(){k(!1)}),k(!0)}}};return k}function z(a,b,c,d){return{restrict:"ECA",priority:-400,compile:function(e){var f=e.html();return function(e,g,h){var i=c.$current,j=A(e,h,g,d),k=i&&i.locals[j];if(k){g.data("$uiView",{name:j,state:k.$$state}),g.html(k.$template?k.$template:f);var l=a(g.contents());if(k.$$controller){k.$scope=e,k.$element=g;var m=b(k.$$controller,k);k.$$controllerAs&&(e[k.$$controllerAs]=m),g.data("$ngControllerController",m),g.children().data("$ngControllerController",m)}l(e)}}}}}function A(a,b,c,d){var e=d(b.uiView||b.name||"")(a),f=c.inheritedData("$uiView");return e.indexOf("@")>=0?e:e+"@"+(f?f.state.name:"")}function B(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/),!c||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function C(a){var b=a.parent().inheritedData("$uiView");return b&&b.state&&b.state.name?b.state:void 0}function D(a){var b="[object SVGAnimatedString]"===Object.prototype.toString.call(a.prop("href")),c="FORM"===a[0].nodeName;return{attr:c?"action":b?"xlink:href":"href",isAnchor:"A"===a.prop("tagName").toUpperCase(),clickable:!c}}function E(a,b,c,d,e){return function(f){var g=f.which||f.button,h=e();if(!(g>1||f.ctrlKey||f.metaKey||f.shiftKey||a.attr("target"))){var i=c(function(){b.go(h.state,h.params,h.options)});f.preventDefault();var j=d.isAnchor&&!h.href?1:0;f.preventDefault=function(){j--<=0&&c.cancel(i)}}}}function F(a,b){return{relative:C(a)||b.$current,inherit:!0}}function G(a,c){return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(d,e,f,g){var h=B(f.uiSref,a.current.name),i={state:h.state,href:null,params:null},j=D(e),k=g[1]||g[0];i.options=R(F(e,a),f.uiSrefOpts?d.$eval(f.uiSrefOpts):{});var l=function(c){c&&(i.params=b.copy(c)),i.href=a.href(h.state,i.params,i.options),k&&k.$$addStateInfo(h.state,i.params),null!==i.href&&f.$set(j.attr,i.href)};h.paramExpr&&(d.$watch(h.paramExpr,function(a){a!==i.params&&l(a)},!0),i.params=b.copy(d.$eval(h.paramExpr))),l(),j.clickable&&e.bind("click",E(e,a,c,j,function(){return i}))}}}function H(a,b){return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(c,d,e,f){function g(b){l.state=b[0],l.params=b[1],l.options=b[2],l.href=a.href(l.state,l.params,l.options),i&&i.$$addStateInfo(l.state,l.params),l.href&&e.$set(h.attr,l.href)}var h=D(d),i=f[1]||f[0],j=[e.uiState,e.uiStateParams||null,e.uiStateOpts||null],k="["+j.map(function(a){return a||"null"}).join(", ")+"]",l={state:null,params:null,options:null,href:null};c.$watch(k,g,!0),g(c.$eval(k)),h.clickable&&d.bind("click",E(d,a,b,h,function(){return l}))}}}function I(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs","$timeout",function(b,d,e,f){function g(b,c,e){var f=a.get(b,C(d)),g=h(b,c);p.push({state:f||{name:b},params:c,hash:g}),q[g]=e}function h(a,c){if(!N(a))throw new Error("state should be a string");return O(c)?a+T(c):(c=b.$eval(c),O(c)?a+T(c):a)}function i(){for(var a=0;a0||(g(a,b,o),i())},b.$on("$stateChangeSuccess",i),i()}]}}function J(a){var b=function(b,c){return a.is(b,c)};return b.$stateful=!0,b}function K(a){var b=function(b,c,d){return a.includes(b,c,d)};return b.$stateful=!0,b}var L=b.isDefined,M=b.isFunction,N=b.isString,O=b.isObject,P=b.isArray,Q=b.forEach,R=b.extend,S=b.copy,T=b.toJson;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),p.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",p),q.$inject=["$http","$templateCache","$injector"],b.module("ui.router.util").service("$templateFactory",q);var U;r.prototype.concat=function(a,b){var c={caseInsensitive:U.caseInsensitive(),strict:U.strictMode(),squash:U.defaultSquashPolicy()};return new r(this.sourcePath+a+this.sourceSearch,R(c,b),this)},r.prototype.toString=function(){return this.source},r.prototype.exec=function(a,b){function c(a){function b(a){return a.split("").reverse().join("")}function c(a){return a.replace(/\\-/g,"-")}var d=b(a).split(/-(?!\\)/),e=o(d,b);return o(e,c).reverse()}var d=this.regexp.exec(a);if(!d)return null;b=b||{};var e,f,g,h=this.parameters(),i=h.length,j=this.segments.length-1,k={};if(j!==d.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");var l,m;for(e=0;j>e;e++){for(g=h[e],l=this.params[g],m=d[e+1],f=0;fe;e++){for(g=h[e],k[g]=this.params[g].value(b[g]),l=this.params[g],m=b[g],f=0;ff;f++){var k=h>f,l=d[f],m=e[l],n=m.value(a[l]),p=m.isOptional&&m.type.equals(m.value(),n),q=p?m.squash:!1,r=m.type.encode(n);if(k){var s=c[f+1],t=f+1===h;if(q===!1)null!=r&&(j+=P(r)?o(r,b).join("-"):encodeURIComponent(r)),j+=s;else if(q===!0){var u=j.match(/\/$/)?/\/?(.*)/:/(.*)/;j+=s.match(u)[1]}else N(q)&&(j+=q+s);t&&m.squash===!0&&"/"===j.slice(-1)&&(j=j.slice(0,-1))}else{if(null==r||p&&q!==!1)continue;if(P(r)||(r=[r]),0===r.length)continue;r=o(r,encodeURIComponent).join("&"+l+"="),j+=(g?"&":"?")+(l+"="+r),g=!0}}return j},s.prototype.is=function(a,b){return!0},s.prototype.encode=function(a,b){return a},s.prototype.decode=function(a,b){return a},s.prototype.equals=function(a,b){return a==b},s.prototype.$subPattern=function(){var a=this.pattern.toString();return a.substr(1,a.length-2)},s.prototype.pattern=/.*/,s.prototype.toString=function(){return"{Type:"+this.name+"}"},s.prototype.$normalize=function(a){return this.is(a)?a:this.decode(a)},s.prototype.$asArray=function(a,b){function d(a,b){function d(a,b){return function(){return a[b].apply(a,arguments)}}function e(a){return P(a)?a:L(a)?[a]:[]}function f(a){switch(a.length){case 0:return c;case 1:return"auto"===b?a[0]:a;default:return a}}function g(a){return!a}function h(a,b){return function(c){if(P(c)&&0===c.length)return c;c=e(c);var d=o(c,a);return b===!0?0===n(d,g).length:f(d)}}function i(a){return function(b,c){var d=e(b),f=e(c);if(d.length!==f.length)return!1;for(var g=0;g0},this.rename=function(a,b){return a.ngfName=b,a},this.jsonBlob=function(a){null==a||angular.isString(a)||(a=JSON.stringify(a));var b=new window.Blob([a],{type:"application/json"});return b._ngfBlob=!0,b},this.json=function(a){return angular.toJson(a)},this.isFile=function(a){return null!=a&&(a instanceof window.Blob||a.flashId&&a.name&&a.size)},this.upload=function(a,b){function c(b,c){if(b._ngfBlob)return b;if(a._file=a._file||b,null!=a._start&&g){a._end&&a._end>=b.size&&(a._finished=!0,a._end=b.size);var d=b.slice(a._start,a._end||b.size);return d.name=b.name,d.ngfName=b.ngfName,a._chunkSize&&(c.append("_chunkSize",a._chunkSize),c.append("_currentChunkSize",a._end-a._start),c.append("_chunkNumber",Math.floor(a._start/a._chunkSize)),c.append("_totalSize",a._file.size)),d}return b}function h(b,d,e){if(void 0!==d)if(angular.isDate(d)&&(d=d.toISOString()),angular.isString(d))b.append(e,d);else if(f.isFile(d)){var g=c(d,b),i=e.split(",");i[1]&&(g.ngfName=i[1].replace(/^\s+|\s+$/g,""),e=i[0]),a._fileKey=a._fileKey||e,b.append(e,g,g.ngfName||g.name)}else if(angular.isObject(d)){if(d.$$ngfCircularDetection)throw"ngFileUpload: Circular reference in config.data. Make sure specified data for Upload.upload() has no circular reference: "+e;d.$$ngfCircularDetection=!0;try{for(var j in d)if(d.hasOwnProperty(j)&&"$$ngfCircularDetection"!==j){var k=null==a.objectKey?"[i]":a.objectKey;d.length&&parseInt(j)>-1&&(k=null==a.arrayKey?k:a.arrayKey),h(b,d[j],e+k.replace(/[ik]/g,j))}}finally{delete d.$$ngfCircularDetection}}else b.append(e,d)}function i(){a._chunkSize=f.translateScalars(a.resumeChunkSize),a._chunkSize=a._chunkSize?parseInt(a._chunkSize.toString()):null,a.headers=a.headers||{},a.headers["Content-Type"]=void 0,a.transformRequest=a.transformRequest?angular.isArray(a.transformRequest)?a.transformRequest:[a.transformRequest]:[],a.transformRequest.push(function(b){var c,d=new window.FormData;b=b||a.fields||{},a.file&&(b.file=a.file);for(c in b)if(b.hasOwnProperty(c)){var e=b[c];a.formDataAppender?a.formDataAppender(d,c,e):h(d,e,c)}return d})}return b||(a=e(a)),a._isDigested||(a._isDigested=!0,i()),d(a)},this.http=function(b){return b=e(b),b.transformRequest=b.transformRequest||function(b){return window.ArrayBuffer&&b instanceof window.ArrayBuffer||b instanceof window.Blob?b:a.defaults.transformRequest[0].apply(this,arguments)},b._chunkSize=f.translateScalars(b.resumeChunkSize),b._chunkSize=b._chunkSize?parseInt(b._chunkSize.toString()):null,d(b)},this.translateScalars=function(a){if(angular.isString(a)){if(a.search(/kb/i)===a.length-2)return parseFloat(1024*a.substring(0,a.length-2));if(a.search(/mb/i)===a.length-2)return parseFloat(1048576*a.substring(0,a.length-2));if(a.search(/gb/i)===a.length-2)return parseFloat(1073741824*a.substring(0,a.length-2));if(a.search(/b/i)===a.length-1)return parseFloat(a.substring(0,a.length-1));if(a.search(/s/i)===a.length-1)return parseFloat(a.substring(0,a.length-1));if(a.search(/m/i)===a.length-1)return parseFloat(60*a.substring(0,a.length-1));if(a.search(/h/i)===a.length-1)return parseFloat(3600*a.substring(0,a.length-1))}return a},this.urlToBlob=function(c){var d=b.defer();return a({url:c,method:"get",responseType:"arraybuffer"}).then(function(a){var b=new Uint8Array(a.data),c=a.headers("content-type")||"image/WebP",e=new window.Blob([b],{type:c});d.resolve(e)},function(a){d.reject(a)}),d.promise},this.setDefaults=function(a){this.defaults=a||{}},this.defaults={},this.version=ngFileUpload.version}]),ngFileUpload.service("Upload",["$parse","$timeout","$compile","$q","UploadExif",function(a,b,c,d,e){function f(a,b,c){var e=[h.emptyPromise()];return angular.forEach(a,function(d,f){0===d.type.indexOf("image/jpeg")&&h.attrGetter("ngfFixOrientation",b,c,{$file:d})&&e.push(h.happyPromise(h.applyExifRotation(d),d).then(function(b){a.splice(f,1,b)}))}),d.all(e)}function g(a,b,c){function e(d,e){if(0===d.type.indexOf("image")){if(f.pattern&&!h.validatePattern(d,f.pattern))return;var i=h.resize(d,f.width,f.height,f.quality,f.type,f.ratio,f.centerCrop,function(a,e){return h.attrGetter("ngfResizeIf",b,c,{$width:a,$height:e,$file:d})},f.restoreExif!==!1);g.push(i),i.then(function(b){a.splice(e,1,b)},function(a){d.$error="resize",d.$errorParam=(a?(a.message?a.message:a)+": ":"")+(d&&d.name)})}}var f=h.attrGetter("ngfResize",b,c);if(!(f&&angular.isObject(f)&&h.isResizeSupported()&&a.length))return h.emptyPromise();for(var g=[h.emptyPromise()],i=0;i-1:!0},h.emptyPromise=function(){var a=d.defer(),c=arguments;return b(function(){a.resolve.apply(a,c)}),a.promise},h.rejectPromise=function(){var a=d.defer(),c=arguments;return b(function(){a.reject.apply(a,c)}),a.promise},h.happyPromise=function(a,c){var e=d.defer();return a.then(function(a){e.resolve(a)},function(a){b(function(){throw a}),e.resolve(c)}),e.promise},h.updateModel=function(c,d,e,i,j,k,l){function m(f,g,j,l,m){d.$$ngfPrevValidFiles=f,d.$$ngfPrevInvalidFiles=g;var n=f&&f.length?f[0]:null,o=g&&g.length?g[0]:null;c&&(h.applyModelValidation(c,f),c.$setViewValue(m?n:f)),i&&a(i)(e,{$files:f,$file:n,$newFiles:j,$duplicateFiles:l,$invalidFiles:g,$invalidFile:o,$event:k});var p=h.attrGetter("ngfModelInvalid",d);p&&b(function(){a(p).assign(e,m?o:g)}),b(function(){})}function n(){function a(a,b){return a.name===b.name&&(a.$ngfOrigSize||a.size)===(b.$ngfOrigSize||b.size)&&a.type===b.type}function b(b){var c;for(c=0;c2){var c=d.defaults.androidFixMinorVersion||4;return parseInt(b[1])<4||parseInt(b[1])===c&&parseInt(b[2])');n(a);var c=angular.element("");return c.css("visibility","hidden").css("position","absolute").css("overflow","hidden").css("width","0px").css("height","0px").css("border","none").css("margin","0px").css("padding","0px").attr("tabindex","-1"),g.push({el:b,ref:c}),document.body.appendChild(c.append(a)[0]),a}function p(c){if(b.attr("disabled"))return!1;if(!t("ngfSelectDisabled",a)){var d=q(c);if(null!=d)return d;r(c);try{k()||document.body.contains(w[0])||(g.push({el:b,ref:w.parent()}),document.body.appendChild(w.parent()[0]),w.bind("change",m))}catch(f){}return e(navigator.userAgent)?setTimeout(function(){w[0].click()},0):w[0].click(),!1}}function q(a){var b=a.changedTouches||a.originalEvent&&a.originalEvent.changedTouches;if("touchstart"===a.type)return v=b?b[0].clientY:0,!0;if(a.stopPropagation(),a.preventDefault(),"touchend"===a.type){var c=b?b[0].clientY:0;if(Math.abs(c-v)>20)return!1}}function r(b){j.shouldUpdateOn("click",c,a)&&w.val()&&(w.val(null),j.updateModel(d,c,a,l(),null,b,!0))}function s(a){if(w&&!w.attr("__ngf_ie10_Fix_")){if(!w[0].parentNode)return void(w=null);a.preventDefault(),a.stopPropagation(),w.unbind("click");var b=w.clone();return w.replaceWith(b),w=b,w.attr("__ngf_ie10_Fix_","true"),w.bind("change",m),w.bind("click",s),w[0].click(),!1}w.removeAttr("__ngf_ie10_Fix_")}var t=function(a,b){return j.attrGetter(a,c,b)};j.registerModelChangeValidator(d,c,a);var u=[];u.push(a.$watch(t("ngfMultiple"),function(){w.attr("multiple",t("ngfMultiple",a))})),u.push(a.$watch(t("ngfCapture"),function(){w.attr("capture",t("ngfCapture",a))})),u.push(a.$watch(t("ngfAccept"),function(){w.attr("accept",t("ngfAccept",a))})),c.$observe("accept",function(){w.attr("accept",t("accept"))}),u.push(function(){c.$$observers&&delete c.$$observers.accept});var v=0,w=b;k()||(w=o()),w.bind("change",m),k()?b.bind("click",r):b.bind("click touchstart touchend",p),-1!==navigator.appVersion.indexOf("MSIE 10")&&w.bind("click",s),d&&d.$formatters.push(function(a){return(null==a||0===a.length)&&w.val()&&w.val(null),a}),a.$on("$destroy",function(){k()||w.parent().remove(),angular.forEach(u,function(a){a()})}),h(function(){for(var a=0;ab||d.blobUrls.length>e)&&d.blobUrls.length>1;){var h=d.blobUrls.splice(0,1)[0];c.revokeObjectURL(h.url),d.blobUrlsTotalSize-=h.size}}})}else{var i=new FileReader;i.onload=function(c){b(function(){a.$ngfDataUrl=c.target.result,g.resolve(c.target.result,a),b(function(){delete a.$ngfDataUrl},1e3)})},i.onerror=function(){b(function(){a.$ngfDataUrl="",g.reject()})},i.readAsDataURL(a)}}else b(function(){a[e?"$ngfDataUrl":"$ngfBlobUrl"]="",g.reject()})}),f=e?a.$$ngfDataUrlPromise=g.promise:a.$$ngfBlobUrlPromise=g.promise,f["finally"](function(){delete a[e?"$$ngfDataUrlPromise":"$$ngfBlobUrlPromise"]}),f},d}]),ngFileUpload.directive("ngfSrc",["Upload","$timeout",function(a,c){return{restrict:"AE",link:function(d,e,f){b(a,c,d,e,f,"ngfSrc",a.attrGetter("ngfResize",f,d),!1)}}}]),ngFileUpload.directive("ngfBackground",["Upload","$timeout",function(a,c){return{restrict:"AE",link:function(d,e,f){b(a,c,d,e,f,"ngfBackground",a.attrGetter("ngfResize",f,d),!0)}}}]),ngFileUpload.directive("ngfThumbnail",["Upload","$timeout",function(a,c){return{restrict:"AE",link:function(d,e,f){var g=a.attrGetter("ngfSize",f,d);b(a,c,d,e,f,"ngfThumbnail",g,a.attrGetter("ngfAsBackground",f,d))}}}]),ngFileUpload.config(["$compileProvider",function(a){a.imgSrcSanitizationWhitelist&&a.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|local|file|data|blob):/),a.aHrefSanitizationWhitelist&&a.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|local|file|data|blob):/)}]),ngFileUpload.filter("ngfDataUrl",["UploadDataUrl","$sce",function(a,b){return function(c,d,e){if(angular.isString(c))return b.trustAsResourceUrl(c);var f=c&&((d?c.$ngfDataUrl:c.$ngfBlobUrl)||c.$ngfDataUrl);return c&&!f?(!c.$ngfDataUrlFilterInProgress&&angular.isObject(c)&&(c.$ngfDataUrlFilterInProgress=!0,a.dataUrl(c,d)),""):(c&&delete c.$ngfDataUrlFilterInProgress,(c&&f?e?b.trustAsResourceUrl(f):f:c)||"")}}])}(),ngFileUpload.service("UploadValidate",["UploadDataUrl","$q","$timeout",function(a,b,c){function d(a){var b="",c=[];if(a.length>2&&"/"===a[0]&&"/"===a[a.length-1])b=a.substring(1,a.length-1);else{var e=a.split(",");if(e.length>1)for(var f=0;f|:\\-]","g"),"\\$&")+"$",b=b.replace(/\\\*/g,".*").replace(/\\\?/g,"."))}return{regexp:b,excludes:c}}function e(a,b){null==b||a.$dirty||(a.$setDirty?a.$setDirty():a.$dirty=!0)}var f=a;return f.validatePattern=function(a,b){if(!b)return!0;var c=d(b),e=!0;if(c.regexp&&c.regexp.length){var f=new RegExp(c.regexp,"i");e=null!=a.type&&f.test(a.type)||null!=a.name&&f.test(a.name)}for(var g=c.excludes.length;g--;){var h=new RegExp(c.excludes[g],"i");e=e&&(null==a.type||h.test(a.type))&&(null==a.name||h.test(a.name))}return e},f.ratioToFloat=function(a){var b=a.toString(),c=b.search(/[x:]/i);return b=c>-1?parseFloat(b.substring(0,c))/parseFloat(b.substring(c+1)):parseFloat(b)},f.registerModelChangeValidator=function(a,b,c){a&&a.$formatters.push(function(d){a.$dirty&&(d&&!angular.isArray(d)&&(d=[d]),f.validate(d,d?d.length:0,a,b,c).then(function(){f.applyModelValidation(a,d)}))})},f.applyModelValidation=function(a,b){e(a,b),angular.forEach(a.$ngfValidations,function(b){a.$setValidity(b.name,b.valid)})},f.getValidationAttr=function(a,b,c,d,e){var g="ngf"+c[0].toUpperCase()+c.substr(1),h=f.attrGetter(g,a,b,{$file:e});if(null==h&&(h=f.attrGetter("ngfValidate",a,b,{$file:e}))){var i=(d||c).split(".");h=h[i[0]],i.length>1&&(h=h&&h[i[1]])}return h},f.validate=function(a,c,d,e,g){function h(b,c,h){if(a){for(var i=a.length,j=null;i--;){var k=a[i];if(k){var l=f.getValidationAttr(e,g,b,c,k);null!=l&&(h(k,l)||(k.$error=b,(k.$errorMessages=k.$errorMessages||{}).name=!0,k.$errorParam=l,a.splice(i,1),j=!1))}}null!==j&&d.$ngfValidations.push({name:b,valid:j})}}function i(c,h,i,k,l){function m(a,b,d){null!=d?k(b,d).then(function(e){l(e,d)?a.resolve():(b.$error=c,(b.$errorMessages=b.$errorMessages||{}).name=!0,b.$errorParam=d,a.reject())},function(){j("ngfValidateForce",{$file:b})?(b.$error=c,(b.$errorMessages=b.$errorMessages||{}).name=!0,b.$errorParam=d,a.reject()):a.resolve()}):a.resolve()}var n=[f.emptyPromise()];return a?(a=void 0===a.length?[a]:a,angular.forEach(a,function(a){var d=b.defer();return n.push(d.promise),!i||null!=a.type&&0===a.type.search(i)?void("dimensions"===c&&null!=f.attrGetter("ngfDimensions",e)?f.imageDimensions(a).then(function(b){m(d,a,j("ngfDimensions",{$file:a,$width:b.width,$height:b.height}))},function(){d.reject()}):"duration"===c&&null!=f.attrGetter("ngfDuration",e)?f.mediaDuration(a).then(function(b){m(d,a,j("ngfDuration",{$file:a,$duration:b}))},function(){d.reject()}):m(d,a,f.getValidationAttr(e,g,c,h,a))):void d.resolve()}),b.all(n).then(function(){d.$ngfValidations.push({name:c,valid:!0})},function(){d.$ngfValidations.push({name:c,valid:!1})})):void 0}d=d||{},d.$ngfValidations=d.$ngfValidations||[],angular.forEach(d.$ngfValidations,function(a){a.valid=!0});var j=function(a,b){return f.attrGetter(a,e,g,b)};if(null==a||0===a.length)return f.emptyPromise(d);a=void 0===a.length?[a]:a.slice(0),h("maxFiles",null,function(a,b){return b>=c}),h("pattern",null,f.validatePattern),h("minSize","size.min",function(a,b){return a.size+.1>=f.translateScalars(b)}),h("maxSize","size.max",function(a,b){return a.size-.1<=f.translateScalars(b)});var k=0;if(h("maxTotalSize",null,function(b,c){return k+=b.size,k>f.translateScalars(c)?(a.splice(0,a.length),!1):!0}),h("validateFn",null,function(a,b){return b===!0||null===b||""===b}),!a.length)return f.emptyPromise(d,d.$ngfValidations);var l=b.defer(),m=[];return m.push(f.happyPromise(i("maxHeight","height.max",/image/,this.imageDimensions,function(a,b){return a.height<=b}))),m.push(f.happyPromise(i("minHeight","height.min",/image/,this.imageDimensions,function(a,b){return a.height>=b}))),m.push(f.happyPromise(i("maxWidth","width.max",/image/,this.imageDimensions,function(a,b){return a.width<=b}))),m.push(f.happyPromise(i("minWidth","width.min",/image/,this.imageDimensions,function(a,b){return a.width>=b}))),m.push(f.happyPromise(i("dimensions",null,/image/,function(a,b){return f.emptyPromise(b)},function(a){return a}))),m.push(f.happyPromise(i("ratio",null,/image/,this.imageDimensions,function(a,b){for(var c=b.toString().split(","),d=!1,e=0;e-1e-4}))),m.push(f.happyPromise(i("maxDuration","duration.max",/audio|video/,this.mediaDuration,function(a,b){return a<=f.translateScalars(b)}))),m.push(f.happyPromise(i("minDuration","duration.min",/audio|video/,this.mediaDuration,function(a,b){return a>=f.translateScalars(b)}))),m.push(f.happyPromise(i("duration",null,/audio|video/,function(a,b){return f.emptyPromise(b)},function(a){return a}))),m.push(f.happyPromise(i("validateAsyncFn",null,null,function(a,b){return b},function(a){return a===!0||null===a||""===a}))),b.all(m).then(function(){l.resolve(d,d.$ngfValidations)})},f.imageDimensions=function(a){if(a.$ngfWidth&&a.$ngfHeight){var d=b.defer();return c(function(){d.resolve({width:a.$ngfWidth,height:a.$ngfHeight})}),d.promise}if(a.$ngfDimensionPromise)return a.$ngfDimensionPromise;var e=b.defer();return c(function(){return 0!==a.type.indexOf("image")?void e.reject("not image"):void f.dataUrl(a).then(function(b){function d(){var b=h[0].clientWidth,c=h[0].clientHeight;h.remove(),a.$ngfWidth=b,a.$ngfHeight=c,e.resolve({width:b,height:c})}function f(){h.remove(),e.reject("load error")}function g(){c(function(){h[0].parentNode&&(h[0].clientWidth?d():i>10?f():g())},1e3)}var h=angular.element("").attr("src",b).css("visibility","hidden").css("position","fixed");h.on("load",d),h.on("error",f);var i=0;g(),angular.element(document.getElementsByTagName("body")[0]).append(h)},function(){e.reject("load error")})}),a.$ngfDimensionPromise=e.promise,a.$ngfDimensionPromise["finally"](function(){delete a.$ngfDimensionPromise}),a.$ngfDimensionPromise},f.mediaDuration=function(a){if(a.$ngfDuration){var d=b.defer();return c(function(){d.resolve(a.$ngfDuration)}),d.promise}if(a.$ngfDurationPromise)return a.$ngfDurationPromise;var e=b.defer();return c(function(){return 0!==a.type.indexOf("audio")&&0!==a.type.indexOf("video")?void e.reject("not media"):void f.dataUrl(a).then(function(b){function d(){var b=h[0].duration;a.$ngfDuration=b,h.remove(),e.resolve(b)}function f(){h.remove(),e.reject("load error")}function g(){c(function(){h[0].parentNode&&(h[0].duration?d():i>10?f():g())},1e3)}var h=angular.element(0===a.type.indexOf("audio")?"