├── router.ex ├── web ├── static │ ├── js │ │ └── app.js │ ├── css │ │ ├── app.css │ │ └── style.scss │ └── assets │ │ ├── favicon.ico │ │ ├── images │ │ ├── 1.jpg │ │ ├── arrow.png │ │ └── phoenix.png │ │ └── robots.txt ├── views │ ├── tag_view.ex │ ├── user_view.ex │ ├── about_view.ex │ ├── layout_view.ex │ ├── search_view.ex │ ├── share_view.ex │ ├── achieve_view.ex │ ├── category_view.ex │ ├── comment_view.ex │ ├── project_view.ex │ ├── user │ │ ├── link_view.ex │ │ ├── tag_view.ex │ │ ├── upload_view.ex │ │ ├── category_view.ex │ │ ├── profile_view.ex │ │ ├── comment_view.ex │ │ └── article_view.ex │ ├── session_view.ex │ ├── article_view.ex │ ├── error_view.ex │ └── error_helpers.ex ├── controllers │ ├── about_controller.ex │ ├── project_controller.ex │ ├── achieve_controller.ex │ ├── user │ │ ├── profile_controller.ex │ │ ├── tag_controller.ex │ │ ├── link_controller.ex │ │ ├── category_controller.ex │ │ ├── comment_controller.ex │ │ ├── upload_controller.ex │ │ └── article_controller.ex │ ├── comment_controller.ex │ ├── article_controller.ex │ ├── user_controller.ex │ ├── search_controller.ex │ ├── tag_controller.ex │ ├── category_controller.ex │ └── session_controller.ex ├── templates │ ├── user │ │ ├── comment │ │ │ ├── new.html.eex │ │ │ ├── show.html.eex │ │ │ ├── edit.html.eex │ │ │ ├── form.html.eex │ │ │ └── index.html.eex │ │ ├── tag │ │ │ ├── show.html.eex │ │ │ ├── form.html.eex │ │ │ ├── edit.html.eex │ │ │ ├── new.html.eex │ │ │ └── index.html.eex │ │ ├── link │ │ │ ├── show.html.eex │ │ │ ├── edit.html.eex │ │ │ ├── new.html.eex │ │ │ ├── form.html.eex │ │ │ └── index.html.eex │ │ ├── category │ │ │ ├── show.html.eex │ │ │ ├── form.html.eex │ │ │ ├── edit.html.eex │ │ │ ├── new.html.eex │ │ │ └── index.html.eex │ │ ├── new.html.eex │ │ ├── edit.html.eex │ │ ├── upload │ │ │ └── photo_form.html.eex │ │ ├── article │ │ │ ├── new.html.eex │ │ │ ├── edit.html.eex │ │ │ ├── form.html.eex │ │ │ ├── index.html.eex │ │ │ └── show.html.eex │ │ ├── reset_password_from.html.eex │ │ ├── form.html.eex │ │ └── profile │ │ │ └── show.html.eex │ ├── session │ │ ├── new.html.eex │ │ ├── form.html.eex │ │ └── home.html.eex │ ├── share │ │ ├── friend_ship.html.eex │ │ ├── category.html.eex │ │ ├── tag.html.eex │ │ ├── hot_articles.html.eex │ │ ├── user_left_nagiva.html.eex │ │ ├── user_info.html.eex │ │ └── article_body.html.eex │ ├── error │ │ ├── not_found.html.eex │ │ └── internal_error.html.eex │ ├── achieve │ │ └── index.html.eex │ ├── about │ │ └── index.html.eex │ ├── category │ │ ├── index.html.eex │ │ └── show.html.eex │ ├── search │ │ └── index.html.eex │ ├── project │ │ └── index.html.eex │ ├── tag │ │ └── show.html.eex │ ├── article │ │ └── show.html.eex │ └── layout │ │ └── app.html.eex ├── models │ ├── article_tag.ex │ ├── link.ex │ ├── tag.ex │ ├── comment.ex │ ├── category.ex │ ├── article.ex │ └── user.ex ├── plugs │ └── session_plug.ex ├── gettext.ex ├── channels │ └── user_socket.ex ├── web.ex └── router.ex ├── test ├── test_helper.exs ├── views │ ├── page_view_test.exs │ ├── layout_view_test.exs │ └── error_view_test.exs ├── controllers │ ├── page_controller_test.exs │ ├── link_controller_test.exs │ ├── user_controller_test.exs │ ├── article_controller_test.exs │ └── category_controller_test.exs ├── models │ ├── tag_test.exs │ ├── link_test.exs │ ├── user_test.exs │ ├── category_test.exs │ ├── article_test.exs │ └── comment_test.exs └── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── lib ├── hello_phoenix │ ├── repo.ex │ └── endpoint.ex └── hello_phoenix.ex ├── priv ├── repo │ ├── migrations │ │ ├── 20161020083431_add_avatar_to_user.exs │ │ ├── 20161112131723_add_hash_id_to_category.exs │ │ ├── 20161113123809_add_hash_id_to_article.exs │ │ ├── 20161018025553_add_reading_to_article.exs │ │ ├── 20161123144228_add_block_to_article_table.exs │ │ ├── 20161124163228_create_link.exs │ │ ├── 20161025083139_create_tag.exs │ │ ├── 20161017010949_create_category.exs │ │ ├── 20161025083439_create_posts_tags.exs │ │ ├── 20161018132719_create_user.exs │ │ ├── 20161017060028_create_comment.exs │ │ └── 20161017011707_create_article.exs │ └── seeds.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── config ├── test.exs ├── config.exs ├── dev.exs └── prod.exs ├── package.json ├── .gitignore ├── README.md ├── brunch-config.js ├── mix.exs └── mix.lock /router.ex: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/static/js/app.js: -------------------------------------------------------------------------------- 1 | import "phoenix_html" 2 | -------------------------------------------------------------------------------- /web/static/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ -------------------------------------------------------------------------------- /web/views/tag_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.TagView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/about_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.AboutView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.LayoutView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/search_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.SearchView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/share_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ShareView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(HelloPhoenix.Repo, :manual) 4 | 5 | -------------------------------------------------------------------------------- /web/views/achieve_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.AchieveView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/category_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CategoryView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/comment_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CommentView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/project_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ProjectView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/user/link_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.LinkView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/user/tag_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.TagView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebroo/phoenix_blog/HEAD/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /web/static/assets/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebroo/phoenix_blog/HEAD/web/static/assets/images/1.jpg -------------------------------------------------------------------------------- /web/views/user/upload_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.UploadView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/user/category_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.CategoryView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/user/profile_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.ProfileView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.PageViewTest do 2 | use HelloPhoenix.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/static/assets/images/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebroo/phoenix_blog/HEAD/web/static/assets/images/arrow.png -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.LayoutViewTest do 2 | use HelloPhoenix.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/static/assets/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebroo/phoenix_blog/HEAD/web/static/assets/images/phoenix.png -------------------------------------------------------------------------------- /lib/hello_phoenix/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo do 2 | use Ecto.Repo, otp_app: :hello_phoenix 3 | use Scrivener, page_size: 10 4 | end 5 | -------------------------------------------------------------------------------- /web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.SessionView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def category_name(article) do 5 | article.category.name 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/views/user/comment_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.CommentView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def article_name(comment) do 5 | comment.article.title 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/controllers/about_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.AboutController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | def index(conn, _params) do 5 | conn 6 | |> render(:index) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /web/views/article_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ArticleView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def markdown(body) do 5 | body 6 | |> Earmark.to_html 7 | |> raw 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /web/controllers/project_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ProjectController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | def index(conn, _params) do 5 | conn 6 | |> render(:index) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /web/static/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.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 | -------------------------------------------------------------------------------- /web/templates/user/comment/new.html.eex: -------------------------------------------------------------------------------- 1 |

New comment

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: user_comment_path(@conn, :create) %> 5 | 6 | <%= link "Back", to: user_comment_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161020083431_add_avatar_to_user.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.AddAvatarToUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :avatar, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.PageControllerTest do 2 | use HelloPhoenix.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161112131723_add_hash_id_to_category.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.AddHashIdToCategory do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:categorys) do 6 | add :hash_id, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161113123809_add_hash_id_to_article.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.AddHashIdToArticle do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:articles) do 6 | add :hash_id, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161018025553_add_reading_to_article.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.AddReadingToArticle do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:articles) do 6 | add :reading, :integer, default: 0 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161123144228_add_block_to_article_table.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.AddBlockToArticleTable do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:articles) do 6 | add :block, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /web/templates/user/tag/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show tag

2 | 3 | 11 | 12 | <%= link "Edit", to: user_tag_path(@conn, :edit, @tag) %> 13 | <%= link "Back", to: user_tag_path(@conn, :index) %> 14 | -------------------------------------------------------------------------------- /web/views/user/article_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.ArticleView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def category_name(article) do 5 | article.category.name 6 | end 7 | 8 | def markdown(body) do 9 | body 10 | |> Earmark.to_html 11 | |> raw 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /web/templates/user/link/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show link

2 | 3 | 11 | 12 | <%= link "Edit", to: user_link_path(@conn, :edit, @link) %> 13 | <%= link "Back", to: user_link_path(@conn, :index) %> 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161124163228_create_link.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateLink do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:links) do 6 | add :name, :string 7 | add :url, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161025083139_create_tag.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateTag do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:tags) do 6 | add :name, :string 7 | 8 | timestamps() 9 | end 10 | create unique_index(:tags, [:name]) 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /web/templates/user/category/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show category

2 | 3 | 11 | 12 | <%= link "Edit", to: user_category_path(@conn, :edit, @category) %> 13 | <%= link "Back", to: user_category_path(@conn, :index) %> 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161017010949_create_category.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateCategory do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:categorys) do 6 | add :name, :string 7 | 8 | timestamps() 9 | end 10 | create unique_index(:categorys, [:name]) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161025083439_create_posts_tags.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreatePostsTags do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:posts_tags, primary_key: false) do 6 | add :article_id, references(:articles) 7 | add :tag_id, references(:tags) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161018132719_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :name, :string 7 | add :password, :string 8 | 9 | timestamps() 10 | end 11 | create unique_index(:users, [:name]) 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /web/templates/user/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%= render "form.html", changeset: @changeset, conn: @conn, action: user_path(@conn, :create) %> 6 |
7 |
8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /web/templates/session/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%= render "form.html", changeset: @changeset, action: session_path(@conn, :create), conn: @conn %> 6 |
7 |
8 |
9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # HelloPhoenix.Repo.insert!(%HelloPhoenix.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /web/templates/user/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

修改密码

6 | <%= render "reset_password_from.html", changeset: @changeset, action: user_user_path(@conn, :update) %> 7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /web/models/article_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ArticleTag do 2 | use HelloPhoenix.Web, :model 3 | 4 | schema "posts_tags" do 5 | field :article_id, :integer 6 | field :tag_id, :integer 7 | end 8 | 9 | @doc """ 10 | Builds a changeset based on the `struct` and `params`. 11 | """ 12 | def changeset(struct, params \\ %{}) do 13 | struct 14 | |> cast(params, [:article_id, :tag_id]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161017060028_create_comment.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateComment do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:comments) do 6 | add :name, :string 7 | add :body, :string 8 | add :article_id, references(:articles, on_delete: :nothing) 9 | 10 | timestamps() 11 | end 12 | create index(:comments, [:article_id]) 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161017011707_create_article.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateArticle do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:articles) do 6 | add :title, :string 7 | add :body, :text 8 | add :category_id, references(:categorys, on_delete: :nothing) 9 | 10 | timestamps() 11 | end 12 | create index(:articles, [:category_id]) 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /web/controllers/achieve_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.AchieveController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{Article} 5 | 6 | def index(conn, _params) do 7 | articles = Article 8 | |> where(block: false) 9 | |> order_by(desc: :inserted_at) 10 | |> Repo.all 11 | |> Repo.preload(:category) 12 | 13 | conn 14 | |> assign(:articles, articles) 15 | |> render(:index) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /web/models/link.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Link do 2 | use HelloPhoenix.Web, :model 3 | 4 | schema "links" do 5 | field :name, :string 6 | field :url, :string 7 | 8 | timestamps() 9 | end 10 | 11 | @doc """ 12 | Builds a changeset based on the `struct` and `params`. 13 | """ 14 | def changeset(struct, params \\ %{}) do 15 | struct 16 | |> cast(params, [:name, :url]) 17 | |> validate_required([:name, :url]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ErrorView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def render("404.html", assigns) do 5 | render "not_found.html", assigns 6 | end 7 | 8 | def render("500.html", assigns) do 9 | render "internal_error.html", assigns 10 | end 11 | # In case no render clause matches or no 12 | # template is found, let's render it as 500 13 | def template_not_found(_template, assigns) do 14 | render "500.html", assigns 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /web/plugs/session_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Plugs.Session do 2 | 3 | import Plug.Conn, only: [get_session: 2, halt: 1] 4 | import Phoenix.Controller, only: [redirect: 2] 5 | alias HelloPhoenix.Router.Helpers 6 | 7 | def init(default), do: default 8 | 9 | def call(conn, _params) do 10 | case get_session(conn, :user_id) do 11 | nil -> 12 | conn |> redirect(to: Helpers.session_path(conn, :new)) |> halt 13 | _ -> 14 | conn 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /web/templates/user/comment/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show comment

2 | 3 | 21 | 22 | <%= link "Edit", to: user_comment_path(@conn, :edit, @comment) %> 23 | <%= link "Back", to: user_comment_path(@conn, :index) %> 24 | -------------------------------------------------------------------------------- /web/templates/share/friend_ship.html.eex: -------------------------------------------------------------------------------- 1 | 2 |
3 |
友情链接
4 | 12 |
13 | -------------------------------------------------------------------------------- /test/models/tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.TagTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.Tag 5 | 6 | @valid_attrs %{name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Tag.changeset(%Tag{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Tag.changeset(%Tag{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/link_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.LinkTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.Link 5 | 6 | @valid_attrs %{name: "some content", url: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Link.changeset(%Link{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Link.changeset(%Link{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.User 5 | 6 | @valid_attrs %{name: "some content", password: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/category_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CategoryTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.Category 5 | 6 | @valid_attrs %{name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Category.changeset(%Category{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Category.changeset(%Category{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/tag.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Tag do 2 | use HelloPhoenix.Web, :model 3 | 4 | schema "tags" do 5 | field :name, :string 6 | many_to_many :articles, HelloPhoenix.Article, join_through: "posts_tags", on_delete: :delete_all 7 | 8 | timestamps() 9 | end 10 | 11 | @doc """ 12 | Builds a changeset based on the `struct` and `params`. 13 | """ 14 | def changeset(struct, params \\ %{}) do 15 | struct 16 | |> cast(params, [:name]) 17 | |> validate_required([:name]) 18 | |> unique_constraint(:name) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/models/article_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ArticleTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.Article 5 | 6 | @valid_attrs %{body: "some content", title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Article.changeset(%Article{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Article.changeset(%Article{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/comment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CommentTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.Comment 5 | 6 | @valid_attrs %{body: "some content", name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Comment.changeset(%Comment{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Comment.changeset(%Comment{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/templates/user/tag/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label f, :name, "标签名字", class: "control-label" %> 10 | <%= text_input f, :name, class: "form-control" %> 11 | <%= error_tag f, :name %> 12 |
13 | 14 |
15 | <%= submit "Submit", class: "btn btn-primary" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /web/templates/user/category/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label f, :name,"范畴名字", class: "control-label" %> 10 | <%= text_input f, :name, class: "form-control" %> 11 | <%= error_tag f, :name %> 12 |
13 | 14 |
15 | <%= submit "Submit", class: "btn btn-primary" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /web/models/comment.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Comment do 2 | use HelloPhoenix.Web, :model 3 | 4 | schema "comments" do 5 | field :name, :string 6 | field :body, :string 7 | belongs_to :article, HelloPhoenix.Article, foreign_key: :article_id 8 | 9 | timestamps() 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:name, :body, :article_id]) 18 | |> validate_required([:name, :body]) 19 | |> validate_length(:body, min: 5) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/templates/share/category.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
分类
3 | 11 |
12 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :hello_phoenix, HelloPhoenix.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :hello_phoenix, HelloPhoenix.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "hello_phoenix_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /web/templates/share/tag.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
标签
3 | 13 |
14 | -------------------------------------------------------------------------------- /web/controllers/user/profile_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.ProfileController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{User} 5 | 6 | def show(conn, _params) do 7 | user = Repo.get!(User, get_session(conn, :user_id)) 8 | changeset = User.changeset(%User{}) 9 | 10 | passwd_changeset = User 11 | |> Repo.get!(get_session(conn, :user_id)) 12 | |> User.changeset 13 | 14 | conn 15 | |> assign(:changeset, changeset) 16 | |> assign(:passwd_changeset, passwd_changeset) 17 | |> assign(:avatar, user.avatar) 18 | |> render(:show) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "jquery": "^3.1.1", 10 | "phoenix": "file:deps/phoenix", 11 | "phoenix_html": "file:deps/phoenix_html", 12 | "sass-brunch": "^2.7.0" 13 | }, 14 | "devDependencies": { 15 | "babel-brunch": "~6.0.0", 16 | "babel-preset-es2015": "^6.18.0", 17 | "brunch": "2.7.4", 18 | "clean-css-brunch": "~2.0.0", 19 | "css-brunch": "~2.0.0", 20 | "javascript-brunch": "~2.0.0", 21 | "uglify-js-brunch": "~2.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/templates/share/hot_articles.html.eex: -------------------------------------------------------------------------------- 1 | 2 |
3 |
热门文章
4 | 14 |
15 | -------------------------------------------------------------------------------- /web/templates/user/tag/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 编辑标签 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, action: user_tag_path(@conn, :update, @tag) %> 12 |
13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ErrorViewTest do 2 | use HelloPhoenix.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(HelloPhoenix.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(HelloPhoenix.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(HelloPhoenix.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/templates/user/link/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 编辑标签 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, action: user_link_path(@conn, :update, @link) %> 12 |
13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /web/templates/user/category/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 编辑范畴 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, action: user_category_path(@conn, :update, @category) %> 12 |
13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /web/templates/user/comment/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 编辑评论 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, action: user_comment_path(@conn, :update, @comment) %> 12 |
13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | config/dev.exs 3 | /_build 4 | /db 5 | /deps 6 | /*.ez 7 | 8 | # Generated on crash by the VM 9 | erl_crash.dump 10 | 11 | # Static artifacts 12 | /node_modules 13 | 14 | # Since we are building assets from web/static, 15 | # we ignore priv/static. You may want to comment 16 | # this depending on your deployment strategy. 17 | /priv/static/ 18 | 19 | # The config/prod.secret.exs file by default contains sensitive 20 | # data and you should not commit it into version control. 21 | # 22 | # Alternatively, you may comment the line below and commit the 23 | # secrets file as long as you replace its contents by environment 24 | # variables. 25 | /config/prod.secret.exs 26 | web/static/css/*.map 27 | config/dev.exs 28 | -------------------------------------------------------------------------------- /web/templates/user/link/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 创建标签 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, 12 | action: user_link_path(@conn, :create) %> 13 |
14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/user/tag/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 创建标签 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, 12 | action: user_tag_path(@conn, :create) %> 13 |
14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/user/category/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 编辑范畴 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, 12 | action: user_category_path(@conn, :create) %> 13 |
14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/user/upload/photo_form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, user_upload_path(@conn, :update_avatar), [multipart: true], fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 |

Profile picture

10 | > 11 | <%= file_input f, :avatar, class: "file", id: "upload_button" %> 12 |
13 | 14 |
15 | <%= submit "Update Profile", class: "btn-sm btn btn-primary", id: "update_profile" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HelloPhoenix 2 | 3 | To start your Phoenix app: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Install Node.js dependencies with `npm install` 8 | * Start Phoenix endpoint with `mix phoenix.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 13 | 14 | ### Usage 15 | 项目的css使用scss编写,安装部署之先执行 16 | scss style.scss style.css 17 | 18 | 如果需要自己修改scss,因为brunch不能把scss文件时时编译为对应的css,导致我在写scss的时候,必需手动scss xxx.scss xxx.css,每次修改累的半死,于是自己鲁了个notify脚本[notifyScss](https://github.com/firebroo/notifyScss) 19 | -------------------------------------------------------------------------------- /web/templates/session/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, [class: "form-signin"], fn f -> %> 2 |

登陆

3 | <%= if @changeset.action do %> 4 |
5 |

Oops, something went wrong! Please check the errors below.

6 |
7 | <% end %> 8 | 9 | <%= text_input f, :name, class: "form-control",placeholder: "用户名" %> 10 | <%= error_tag f, :name %> 11 | 12 | <%= password_input f, :password, class: "form-control", placeholder: "密码" %> 13 | <%= error_tag f, :password %> 14 | 15 |
16 | <%= submit "登陆", class: "btn btn-lg btn-primary" %> 17 | 还没有账户? 点击<%= link "注册", to: user_path(@conn, :new) %> 18 |
19 | <% end %> 20 | -------------------------------------------------------------------------------- /web/templates/user/link/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label f, :name, "人物", class: "control-label" %> 10 | <%= text_input f, :name, class: "form-control" %> 11 | <%= error_tag f, :name %> 12 |
13 | 14 |
15 | <%= label f, :url, "链接", class: "control-label" %> 16 | <%= text_input f, :url, class: "form-control" %> 17 | <%= error_tag f, :url %> 18 |
19 | 20 |
21 | <%= submit "Submit", class: "btn btn-primary" %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /web/templates/user/comment/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label f, :name, "评论人", class: "control-label" %> 10 | <%= text_input f, :name, class: "form-control" %> 11 | <%= error_tag f, :name %> 12 |
13 | 14 |
15 | <%= label f, :body, "评论内容", class: "control-label" %> 16 | <%= text_input f, :body, class: "form-control" %> 17 | <%= error_tag f, :body %> 18 |
19 | 20 |
21 | <%= submit "Submit", class: "btn btn-primary" %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /web/templates/user/article/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 创建辑文章 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, tags: @tags, exist_tags: @exist_tags, 12 | action: user_article_path(@conn, :create), categorys: @categorys %> 13 |
14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/user/article/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
8 | 编辑文章 9 |
10 |
11 | <%= render "form.html", changeset: @changeset, categorys: @categorys, tags: @tags, exist_tags: @exist_tags, 12 | action: user_article_path(@conn, :update, @article) %> 13 |
14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import HelloPhoenix.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :hello_phoenix 24 | end 25 | -------------------------------------------------------------------------------- /web/templates/share/user_left_nagiva.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
菜单
4 |
5 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /web/templates/error/not_found.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 404 Not Found Error 4 | " rel="stylesheet"> 5 | 6 | 7 | 14 | 15 |
16 |
17 |
18 |

您查询的页面不存在。 三秒后跳转主页

19 |
20 |
21 |
22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/templates/error/internal_error.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 500 Internal Error 4 | " rel="stylesheet"> 5 | 6 | 7 | 14 | 15 |
16 |
17 |
18 |

网站貌似出了点问题。 三秒后跳转主页

19 |
20 |
21 |
22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/controllers/comment_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CommentController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{Comment, Article} 5 | 6 | def create(conn, %{"category_id" => category_id, "article_id" => article_hash_id, "comment" => comment_params}) do 7 | article = Article |> where(hash_id: ^article_hash_id) |> Repo.one! 8 | comment_params = comment_params 9 | |> Map.put("article_id", article.id) 10 | 11 | changeset = Comment.changeset(%Comment{}, comment_params) 12 | case Repo.insert(changeset) do 13 | {:ok, _comment} -> 14 | conn 15 | |> put_flash(:info, "评论提交成功.") 16 | {:error, _changeset} -> 17 | conn 18 | |> put_flash(:error, "评论提交失败") 19 | end 20 | |> redirect(to: category_article_path(conn, :show, category_id, article_hash_id)) 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /web/templates/achieve/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
共<%= Enum.count @articles %>篇文章
6 |
7 | <%= for article <- @articles do %> 8 |
9 |
10 |
11 |
12 | 13 |
<%= article.title %>
14 |
15 | <%= article.inserted_at.year %>-<%= article.inserted_at.month %>-<%= article.inserted_at.day %> 16 |
17 |
18 | <% end %> 19 |
20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /web/templates/user/reset_password_from.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 |
8 | <%= label f, :old_password, class: "control-label" %> 9 | <%= password_input f, :old_password, class: "form-control" %> 10 | <%= error_tag f, :old_password %> 11 |
12 | 13 |
14 | <%= label f, :new_password, class: "control-label" %> 15 | <%= password_input f, :password, class: "form-control" %> 16 | <%= error_tag f, :password %> 17 |
18 | 19 |
20 | <%= label f, :password_confirmation, class: "control-label" %> 21 | <%= password_input f, :password_confirmation, class: "form-control" %> 22 | <%= error_tag f, :password_confirmation %> 23 |
24 | 25 |
26 | <%= submit "Submit", class: "btn btn-primary" %> 27 |
28 | <% end %> 29 | -------------------------------------------------------------------------------- /web/templates/about/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 11 |
12 |
13 |
关于
14 |
15 |
16 |

编程技能

17 |
18 |

C,Ruby,Python,Java,Elixir,Haskell,PHP,Reverse Engineering

19 |
20 |

工作

21 |

信息安全从业者。

22 |
23 |
24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :hello_phoenix, 10 | ecto_repos: [HelloPhoenix.Repo] 11 | 12 | # Configures the endpoint 13 | config :hello_phoenix, HelloPhoenix.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "TWKR/qgHskCMAma/nWZVk/TTirKVtM7QV60od+Ry3M4gOe+aMkQC3UvlNJgT2DzP", 16 | render_errors: [view: HelloPhoenix.ErrorView, accepts: ~w(html json)], 17 | pubsub: [name: HelloPhoenix.PubSub, 18 | adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:request_id] 24 | 25 | config :scrivener_html, 26 | routes_helper: HelloPhoenix.Router.Helpers 27 | 28 | # Import environment specific config. This must remain at the bottom 29 | # of this file so it overrides the configuration defined above. 30 | import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /web/templates/user/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action,[class: "form-register"], fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 |

注册用户

8 | 9 |
10 | <%= text_input f, :name, class: "form-control",placeholder: "用户名" %> 11 | <%= error_tag f, :name %> 12 |
13 | 14 |
15 | <%= password_input f, :password, class: "form-control", placeholder: "密码" %> 16 | <%= error_tag f, :password %> 17 |
18 | 19 |
20 | <%= password_input f, :password_confirmation, class: "form-control",placeholder: "确认密码" %> 21 | <%= error_tag f, :password_confirmation %> 22 |
23 | 24 |
25 |
26 | <%= submit "注册", class: "btn btn-lg btn-primary" %> 27 | 已有账户? 点击<%= link "登陆", to: session_path(@conn, :new) %> 28 |
29 |
30 | <% end %> 31 | -------------------------------------------------------------------------------- /web/models/category.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Category do 2 | use HelloPhoenix.Web, :model 3 | 4 | schema "categorys" do 5 | field :name, :string, unique: true 6 | field :hash_id, :string 7 | has_many :articles, HelloPhoenix.Article, on_delete: :delete_all 8 | 9 | timestamps() 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:name]) 18 | |> validate_required([:name]) 19 | |> unique_constraint(:name) 20 | end 21 | 22 | @doc """ 23 | 添加hash之后的id 24 | """ 25 | def put_id_hash(%Ecto.Changeset{valid?: false} = changeset, _field) do 26 | changeset 27 | end 28 | 29 | def put_id_hash(%Ecto.Changeset{valid?: true} = changeset, field) do 30 | value = get_field(changeset, field) 31 | changeset |> put_change(:hash_id, :crypto.hash(:sha256, value) |> Base.encode16 |> String.downcase) 32 | end 33 | 34 | def create_changeset(struct, params \\ %{}) do 35 | struct 36 | |> cast(params, [:name]) 37 | |> validate_required([:name]) 38 | |> unique_constraint(:name) 39 | |> put_id_hash(:name) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias HelloPhoenix.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint HelloPhoenix.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(HelloPhoenix.Repo) 36 | 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.Sandbox.mode(HelloPhoenix.Repo, {:shared, self()}) 39 | end 40 | 41 | :ok 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /web/controllers/article_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ArticleController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | # 导入Model 5 | alias HelloPhoenix.{Article, Comment} 6 | 7 | def show(conn, %{"category_id" => category_id, "id" => id}) do 8 | article = Article 9 | |> where(hash_id: ^id) 10 | |> Repo.one! 11 | |> Repo.preload([:category, :tags, :comments]) 12 | 13 | if category_id != article.category.hash_id do 14 | conn 15 | |> put_layout(false) 16 | |> render(HelloPhoenix.ErrorView, "404.html") 17 | else 18 | comments = Comment 19 | |> where(article_id: ^article.id) 20 | |> order_by(asc: :inserted_at) 21 | |> Repo.all 22 | 23 | # 更新文章阅读次数 24 | Article.changeset(article, %{reading: article.reading + 1}) |> Repo.update! 25 | 26 | 27 | changeset = Comment.changeset(%Comment{}) 28 | 29 | conn 30 | |> assign(:article, article) 31 | |> assign(:comments, comments) 32 | |> assign(:changeset, changeset) 33 | |> assign(:category_id, category_id) 34 | |> render("show.html") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /web/templates/category/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= render HelloPhoenix.ShareView, "article_body.html", pagination: @pagination, conn: @conn %> 8 | <%= pagination_links @conn, @pagination, path: &category_path/3, action: :index %> 9 |
10 | 11 |
12 |
13 | 14 | <%= render HelloPhoenix.ShareView, "user_info.html" %> 15 | 16 | <%= render HelloPhoenix.ShareView, "hot_articles.html", hot_articles: @hot_articles, conn: @conn %> 17 | 18 | <%= render HelloPhoenix.ShareView, "category.html", categorys: @categorys, conn: @conn %> 19 | 20 | <%= render HelloPhoenix.ShareView, "tag.html", tags: @tags, conn: @conn %> 21 | 22 | <%= render HelloPhoenix.ShareView, "friend_ship.html", links: @links %> 23 |
24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias HelloPhoenix.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | 28 | import HelloPhoenix.Router.Helpers 29 | 30 | # The default endpoint for testing 31 | @endpoint HelloPhoenix.Endpoint 32 | end 33 | end 34 | 35 | setup tags do 36 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(HelloPhoenix.Repo) 37 | 38 | unless tags[:async] do 39 | Ecto.Adapters.SQL.Sandbox.mode(HelloPhoenix.Repo, {:shared, self()}) 40 | end 41 | 42 | {:ok, conn: Phoenix.ConnTest.build_conn()} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/hello_phoenix.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | supervisor(HelloPhoenix.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(HelloPhoenix.Endpoint, []), 15 | # Start your own worker by calling: HelloPhoenix.Worker.start_link(arg1, arg2, arg3) 16 | # worker(HelloPhoenix.Worker, [arg1, arg2, arg3]), 17 | worker(ConCache, [ 18 | [ttl_check: :timer.seconds(1), ttl: :timer.seconds(5)], 19 | [name: :hello_phoenix] 20 | ]) 21 | ] 22 | 23 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | def config_change(changed, _new, removed) do 32 | HelloPhoenix.Endpoint.config_change(changed, removed) 33 | :ok 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", HelloPhoenix.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # HelloPhoenix.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /lib/hello_phoenix/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hello_phoenix 3 | 4 | socket "/socket", HelloPhoenix.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :hello_phoenix, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | # The session will be stored in the cookie and signed, 34 | # this means its contents can be read but not tampered with. 35 | # Set :encryption_salt if you would also like to encrypt it. 36 | plug Plug.Session, 37 | store: :cookie, 38 | key: "_hello_phoenix_key", 39 | signing_salt: "pMGI6dmJ" 40 | 41 | plug HelloPhoenix.Router 42 | end 43 | -------------------------------------------------------------------------------- /web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(HelloPhoenix.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(HelloPhoenix.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :hello_phoenix, HelloPhoenix.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", 15 | cd: Path.expand("../", __DIR__)]] 16 | 17 | 18 | # Watch static and templates for browser reloading. 19 | config :hello_phoenix, HelloPhoenix.Endpoint, 20 | live_reload: [ 21 | patterns: [ 22 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 23 | ~r{priv/gettext/.*(po)$}, 24 | ~r{web/views/.*(ex)$}, 25 | ~r{web/templates/.*(eex)$} 26 | ] 27 | ] 28 | 29 | # Do not include metadata nor timestamps in development logs 30 | config :logger, :console, format: "[$level] $message\n" 31 | 32 | # Set a higher stacktrace during development. Avoid configuring such 33 | # in production as building large stacktraces may be expensive. 34 | config :phoenix, :stacktrace_depth, 20 35 | 36 | # Configure your database 37 | config :hello_phoenix, HelloPhoenix.Repo, 38 | adapter: Ecto.Adapters.Postgres, 39 | username: "postgres", 40 | password: "postgres", 41 | database: "hello_phoenix_dev", 42 | hostname: "localhost", 43 | pool_size: 10 44 | -------------------------------------------------------------------------------- /web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{User} 5 | 6 | def new(conn, _params) do 7 | changeset = User.changeset(%User{}) 8 | render conn, "new.html", changeset: changeset 9 | end 10 | 11 | def create(conn, %{"user" => user_params}) do 12 | changeset = User.changeset_register(%User{}, user_params) 13 | 14 | case Repo.insert(changeset) do 15 | {:ok, _user} -> 16 | conn 17 | |> put_flash(:info, "创建用户成功,请登陆") 18 | |> redirect(to: session_path(conn, :new)) 19 | {:error, changeset} -> 20 | conn 21 | |> assign(:changeset, changeset) 22 | |> render("new.html") 23 | end 24 | end 25 | 26 | def edit(conn, _params) do 27 | changeset = User 28 | |> Repo.get!(get_session(conn, :user_id)) 29 | |> User.changeset 30 | 31 | conn 32 | |> assign(:changeset, changeset) 33 | |> render("edit.html") 34 | end 35 | 36 | def update(conn, %{"user" => user_params}) do 37 | changeset = User 38 | |> Repo.get!(get_session(conn, :user_id)) 39 | |> User.changeset_reset_password(user_params) 40 | 41 | case Repo.update(changeset) do 42 | {:ok, _user} -> 43 | conn 44 | |> put_flash(:info, "密码修改成功") 45 | |> redirect(to: user_profile_path(conn, :show)) 46 | {:error, changeset} -> 47 | 48 | conn 49 | |> assign(:changeset, changeset) 50 | |> render(:edit) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /web/controllers/search_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.SearchController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{Category, Tag, Article, Link} 5 | 6 | defp common(conn) do 7 | categorys = Category 8 | |> Repo.all 9 | |> Repo.preload(:articles) 10 | 11 | tags = Tag 12 | |> Repo.all 13 | |> Repo.preload(:articles) 14 | 15 | hot_articles = Article 16 | |> where(block: false) 17 | |> order_by(desc: :reading) 18 | |> limit(10) 19 | |> Repo.all 20 | |> Repo.preload(:category) 21 | 22 | links = ConCache.get_or_store(:hello_phoenix, "links", fn() -> 23 | Link 24 | |> Repo.all 25 | end) 26 | 27 | conn 28 | |> assign(:hot_articles, hot_articles) 29 | |> assign(:categorys, categorys) 30 | |> assign(:tags, tags) 31 | |> assign(:links, links) 32 | end 33 | 34 | def index(conn, params) do 35 | conn = common(conn) 36 | pagination = case Map.fetch(params, "q") do 37 | {:ok, query} -> 38 | query = "%" <> query <> "%" 39 | articles = from(a in Article, where: like(a.body, ^query)) 40 | |> where(block: false) 41 | |> order_by(desc: :inserted_at) 42 | |> preload([:comments, :category, :tags]) 43 | |> Repo.paginate(params) 44 | if articles.entries == [] do 45 | [] 46 | else 47 | articles 48 | end 49 | {:error} -> 50 | [] 51 | end 52 | conn 53 | |> assign(:pagination, pagination) 54 | |> render(:index) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /web/templates/search/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= render HelloPhoenix.ShareView, "article_body.html", pagination: @pagination, conn: @conn %> 8 | <%= if @pagination != [] do %> 9 | <%= pagination_links @conn, @pagination, path: &search_path/3, action: :index, q: Map.get(@conn.params, "q") %> 10 | <%= else %> 11 |
12 |
13 |

搜索 "<%= Map.get(@conn.params, "q") %>"

14 |
15 |
16 |

什么也没搜到...

17 |
18 |
19 | <% end %> 20 |
21 |
22 |
23 | 24 | <%= render HelloPhoenix.ShareView, "user_info.html" %> 25 | <%= render HelloPhoenix.ShareView, "hot_articles.html", hot_articles: @hot_articles, conn: @conn %> 26 | <%= render HelloPhoenix.ShareView, "category.html", categorys: @categorys, conn: @conn %> 27 | <%= render HelloPhoenix.ShareView, "tag.html", tags: @tags, conn: @conn %> 28 | 29 | <%= render HelloPhoenix.ShareView, "friend_ship.html", links: @links %> 30 |
31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /web/templates/user/tag/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
标签列表
8 |
9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= for tag <- @tags do %> 20 | 21 | 22 | 23 | 27 | 28 | <% end %> 29 | 30 |
ID名字操作
<%= tag.id %><%= tag.name %> 24 | <%= link "Edit", to: user_tag_path(@conn, :edit, tag), class: "btn btn-warning" %> 25 | <%= link "Delete", to: user_tag_path(@conn, :delete, tag), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger" %> 26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | -------------------------------------------------------------------------------- /brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // http://brunch.io/docs/config#-files- 9 | // joinTo: { 10 | // "js/app.js": /^(web\/static\/js)/, 11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // order: { 16 | // before: [ 17 | // "web/static/vendor/js/jquery-2.1.1.js", 18 | // "web/static/vendor/js/bootstrap.min.js" 19 | // ] 20 | // } 21 | }, 22 | stylesheets: { 23 | joinTo: "css/app.css", 24 | order: { 25 | after: ["web/static/css/app.css"] // concat app.css last 26 | } 27 | }, 28 | templates: { 29 | joinTo: "js/app.js" 30 | } 31 | }, 32 | 33 | conventions: { 34 | // This option sets where we should place non-css and non-js assets in. 35 | // By default, we set this to "/web/static/assets". Files in this directory 36 | // will be copied to `paths.public`, which is "priv/static" by default. 37 | assets: /^(web\/static\/assets)/ 38 | }, 39 | 40 | // Phoenix paths configuration 41 | paths: { 42 | // Dependencies and current project directories to watch 43 | watched: [ 44 | "web/static", 45 | "test/static" 46 | ], 47 | 48 | // Where to compile files to 49 | public: "priv/static" 50 | }, 51 | 52 | // Configure your plugins 53 | plugins: { 54 | babel: { 55 | // Do not use ES6 compiler in vendor code 56 | ignore: [/web\/static\/vendor/] 57 | } 58 | }, 59 | 60 | modules: { 61 | autoRequire: { 62 | "js/app.js": ["web/static/js/app"] 63 | } 64 | }, 65 | 66 | npm: { 67 | enabled: true 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /web/templates/user/category/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
范畴列表
8 |
9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= for category <- @categorys do %> 20 | 21 | 22 | 23 | 27 | 28 | <% end %> 29 | 30 |
ID名字操作
<%= category.id %><%= category.name %> 24 | <%= link "Edit", to: user_category_path(@conn, :edit, category), class: "btn btn-default btn-warning" %> 25 | <%= link "Delete", to: user_category_path(@conn, :delete, category), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger"%> 26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | -------------------------------------------------------------------------------- /web/controllers/user/tag_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.TagController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.Tag 5 | 6 | def index(conn, _params) do 7 | tags = Repo.all(Tag) 8 | render(conn, "index.html", tags: tags) 9 | end 10 | 11 | def new(conn, _params) do 12 | changeset = Tag.changeset(%Tag{}) 13 | render(conn, "new.html", changeset: changeset) 14 | end 15 | 16 | def create(conn, %{"tag" => tag_params}) do 17 | changeset = Tag.changeset(%Tag{}, tag_params) 18 | 19 | case Repo.insert(changeset) do 20 | {:ok, _tag} -> 21 | conn 22 | |> put_flash(:info, "标签创建成功。") 23 | |> redirect(to: user_tag_path(conn, :index)) 24 | {:error, changeset} -> 25 | render(conn, "new.html", changeset: changeset) 26 | end 27 | end 28 | 29 | def show(conn, %{"id" => id}) do 30 | tag = Repo.get!(Tag, id) 31 | render(conn, "show.html", tag: tag) 32 | end 33 | 34 | def edit(conn, %{"id" => id}) do 35 | tag = Repo.get!(Tag, id) 36 | changeset = Tag.changeset(tag) 37 | render(conn, "edit.html", tag: tag, changeset: changeset) 38 | end 39 | 40 | def update(conn, %{"id" => id, "tag" => tag_params}) do 41 | tag = Repo.get!(Tag, id) 42 | changeset = Tag.changeset(tag, tag_params) 43 | 44 | case Repo.update(changeset) do 45 | {:ok, _tag} -> 46 | conn 47 | |> put_flash(:info, "标签更新成功。") 48 | |> redirect(to: user_tag_path(conn, :index)) 49 | {:error, changeset} -> 50 | render(conn, "edit.html", tag: tag, changeset: changeset) 51 | end 52 | end 53 | 54 | def delete(conn, %{"id" => id}) do 55 | tag = Repo.get!(Tag, id) 56 | 57 | # Here we use delete! (with a bang) because we expect 58 | # it to always work (and if it does not, it will raise). 59 | Repo.delete!(tag) 60 | 61 | conn 62 | |> put_flash(:info, "标签删除成功。") 63 | |> redirect(to: user_tag_path(conn, :index)) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /web/templates/user/article/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 |
8 |
<%= label f, :category, "选择范畴", class: "control-label" %>
9 | <%= select f, :category_id, 10 | (for category <- @categorys, do: {category.name, category.id}), 11 | prompt: "选择范畴", class: "select form-control"%> 12 |
13 |
14 |
<%= label f, :title, "标题", class: "control-label" %>
15 | <%= text_input f, :title, class: "form-control",placeholder: "文章标题" %> 16 | <%= error_tag f, :title %> 17 |
18 |
19 |
<%= label f, :title, "选择标签", class: "control-label"%>
20 | <%= for tag <- @tags do %> 21 | <%=if tag in @exist_tags do %> 22 | 25 | <%= else %> 26 | 29 | <%= end %> 30 | <%= end %> 31 |
32 |
33 | <%= label f, :body, "文章内容", class: "control-label" %> 34 | <%= textarea f, :body, class: "form-control", id: "article_body", rows: 20 %> 35 | <%= error_tag f, :body %> 36 |
37 |
38 | 41 |
42 |
43 | <%= submit "Submit", class: "btn-sm btn btn-primary" %> 44 |
45 | <% end %> 46 | -------------------------------------------------------------------------------- /web/controllers/user/link_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.LinkController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.Link 5 | 6 | def index(conn, _params) do 7 | links = Repo.all(Link) 8 | render(conn, "index.html", links: links) 9 | end 10 | 11 | def new(conn, _params) do 12 | changeset = Link.changeset(%Link{}) 13 | render(conn, "new.html", changeset: changeset) 14 | end 15 | 16 | def create(conn, %{"link" => link_params}) do 17 | changeset = Link.changeset(%Link{}, link_params) 18 | 19 | case Repo.insert(changeset) do 20 | {:ok, _link} -> 21 | conn 22 | |> put_flash(:info, "链接创建成功。") 23 | |> redirect(to: user_link_path(conn, :index)) 24 | {:error, changeset} -> 25 | render(conn, "new.html", changeset: changeset) 26 | end 27 | end 28 | 29 | def show(conn, %{"id" => id}) do 30 | link = Repo.get!(Link, id) 31 | render(conn, "show.html", link: link) 32 | end 33 | 34 | def edit(conn, %{"id" => id}) do 35 | link = Repo.get!(Link, id) 36 | changeset = Link.changeset(link) 37 | render(conn, "edit.html", link: link, changeset: changeset) 38 | end 39 | 40 | def update(conn, %{"id" => id, "link" => link_params}) do 41 | link = Repo.get!(Link, id) 42 | changeset = Link.changeset(link, link_params) 43 | 44 | case Repo.update(changeset) do 45 | {:ok, link} -> 46 | conn 47 | |> put_flash(:info, "链接更新成功。") 48 | |> redirect(to: user_link_path(conn, :show, link)) 49 | {:error, changeset} -> 50 | render(conn, "edit.html", link: link, changeset: changeset) 51 | end 52 | end 53 | 54 | def delete(conn, %{"id" => id}) do 55 | link = Repo.get!(Link, id) 56 | 57 | # Here we use delete! (with a bang) because we expect 58 | # it to always work (and if it does not, it will raise). 59 | Repo.delete!(link) 60 | 61 | conn 62 | |> put_flash(:info, "链接删除成功。") 63 | |> redirect(to: user_link_path(conn, :index)) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /web/templates/user/link/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
友链列表
8 |
9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <%= for link <- @links do %> 21 | 22 | 23 | 24 | 25 | 29 | 30 | <% end %> 31 | 32 |
ID人物链接操作
<%= link.id %><%= link.name %><%= link.url %> 26 | <%= link "Edit", to: user_link_path(@conn, :edit, link), class: "btn btn-warning" %> 27 | <%= link "Delete", to: user_link_path(@conn, :delete, link), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger" %> 28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use HelloPhoenix.Web, :controller 9 | use HelloPhoenix.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | end 27 | end 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller 32 | 33 | alias HelloPhoenix.Repo 34 | import Ecto 35 | import Ecto.Query 36 | 37 | import HelloPhoenix.Router.Helpers 38 | import HelloPhoenix.Gettext 39 | end 40 | end 41 | 42 | def view do 43 | quote do 44 | use Phoenix.View, root: "web/templates" 45 | 46 | # Import convenience functions from controllers 47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 48 | 49 | # Use all HTML functionality (forms, tags, etc) 50 | use Phoenix.HTML 51 | 52 | import HelloPhoenix.Router.Helpers 53 | import HelloPhoenix.ErrorHelpers 54 | import HelloPhoenix.Gettext 55 | import Scrivener.HTML 56 | end 57 | end 58 | 59 | def router do 60 | quote do 61 | use Phoenix.Router 62 | end 63 | end 64 | 65 | def channel do 66 | quote do 67 | use Phoenix.Channel 68 | 69 | alias HelloPhoenix.Repo 70 | import Ecto 71 | import Ecto.Query 72 | import HelloPhoenix.Gettext 73 | end 74 | end 75 | 76 | @doc """ 77 | When used, dispatch to the appropriate controller/view/etc. 78 | """ 79 | defmacro __using__(which) when is_atom(which) do 80 | apply(__MODULE__, which, []) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /web/templates/share/user_info.html.eex: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

firebroo

5 |
瘦小的信息安全从业者
6 |
7 |
8 | User Avatar 10 |
11 | 39 |
40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :hello_phoenix, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps()] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {HelloPhoenix, []}, 21 | applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, 22 | :phoenix_ecto, :postgrex, :con_cache]] 23 | end 24 | 25 | # Specifies which paths to compile per environment. 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies. 30 | # 31 | # Type `mix help deps` for examples and options. 32 | defp deps do 33 | [{:phoenix, "~> 1.2.1"}, 34 | {:phoenix_pubsub, "~> 1.0"}, 35 | {:phoenix_ecto, "~> 3.0"}, 36 | {:postgrex, ">= 0.0.0"}, 37 | {:phoenix_html, "~> 2.6"}, 38 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 39 | {:gettext, "~> 0.11"}, 40 | {:con_cache, "~> 0.11.1"}, 41 | {:earmark, "~> 1.0.3"}, 42 | {:comeonin, "~> 2.6"}, 43 | {:scrivener, "~> 2.0"}, 44 | {:scrivener_ecto, "~> 1.0"}, 45 | {:scrivener_html, "~> 1.1"}, 46 | {:cowboy, "~> 1.0"}] 47 | end 48 | 49 | # Aliases are shortcuts or tasks specific to the current project. 50 | # For example, to create, migrate and run the seeds file at once: 51 | # 52 | # $ mix ecto.setup 53 | # 54 | # See the documentation for `Mix` for more info on aliases. 55 | defp aliases do 56 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 57 | "ecto.reset": ["ecto.drop", "ecto.setup"], 58 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias HelloPhoenix.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import HelloPhoenix.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(HelloPhoenix.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(HelloPhoenix.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | Helper for returning list of errors in a struct when given certain data. 40 | 41 | ## Examples 42 | 43 | Given a User schema that lists `:name` as a required field and validates 44 | `:password` to be safe, it would return: 45 | 46 | iex> errors_on(%User{}, %{password: "password"}) 47 | [password: "is unsafe", name: "is blank"] 48 | 49 | You could then write your assertion like: 50 | 51 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 52 | 53 | You can also create the changeset manually and retrieve the errors 54 | field directly: 55 | 56 | iex> changeset = User.changeset(%User{}, password: "password") 57 | iex> {:password, "is unsafe"} in changeset.errors 58 | true 59 | """ 60 | def errors_on(struct, data) do 61 | struct.__struct__.changeset(struct, data) 62 | |> Ecto.Changeset.traverse_errors(&HelloPhoenix.ErrorHelpers.translate_error/1) 63 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /web/templates/category/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= render HelloPhoenix.ShareView, "article_body.html", pagination: @pagination, conn: @conn %> 8 | <%= pagination_links @conn, @pagination, [Map.get(@conn.params, "id")], path: &category_path/4, action: :show %> 9 |
10 | 11 |
12 |
13 | <%= render HelloPhoenix.ShareView, "user_info.html" %> 14 | <%= render HelloPhoenix.ShareView, "hot_articles.html", hot_articles: @hot_articles, conn: @conn %> 15 | 16 |
17 |
分类
18 | 27 |
28 | <%= render HelloPhoenix.ShareView, "tag.html", tags: @tags, conn: @conn %> 29 | 30 | <%= render HelloPhoenix.ShareView, "friend_ship.html", links: @links %> 31 |
32 |
33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /web/templates/project/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 37 |
38 |
39 | 40 |

加载中...

41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /web/controllers/tag_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.TagController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{Category, Article, Tag, ArticleTag, Link} 5 | 6 | defp common(conn) do 7 | # 缓存所有categorys 8 | categorys = ConCache.get_or_store(:hello_phoenix, "categorys", fn() -> 9 | Category 10 | |> Repo.all 11 | |> Repo.preload(:articles) 12 | end) 13 | 14 | # 缓存所有tags 15 | tags = ConCache.get_or_store(:hello_phoenix, "tags", fn() -> 16 | Tag 17 | |> Repo.all 18 | |> Repo.preload(:articles) 19 | end) 20 | 21 | # 缓存热门文章 22 | hot_articles = ConCache.get_or_store(:hello_phoenix, "hot_articles", fn() -> 23 | Article 24 | |> where(block: false) 25 | |> order_by(desc: :reading) 26 | |> limit(10) 27 | |> Repo.all 28 | |> Repo.preload(:category) 29 | end) 30 | 31 | links = ConCache.get_or_store(:hello_phoenix, "links", fn() -> 32 | Link 33 | |> Repo.all 34 | end) 35 | 36 | conn 37 | |> assign(:hot_articles, hot_articles) 38 | |> assign(:categorys, categorys) 39 | |> assign(:tags, tags) 40 | |> assign(:links, links) 41 | end 42 | 43 | def show(conn, params) do 44 | conn = common(conn) 45 | pagination = case Map.fetch(params, "id") do 46 | {:ok, id} -> 47 | tag = Tag |> where(name: ^id) |> Repo.one 48 | Article 49 | |> where(block: false) 50 | |> join(:inner, [a], at in ArticleTag, at.tag_id == ^tag.id and at.article_id == a.id) 51 | |> preload([:category, :tags, :comments]) 52 | |> order_by(desc: :inserted_at) 53 | |> Repo.paginate(params) 54 | {:error} -> 55 | # 缓存所有文章 56 | ConCache.get_or_store(:hello_phoenix, "pagination", fn() -> 57 | Article 58 | |> where(block: false) 59 | |> order_by(desc: :inserted_at) 60 | |> preload([:category, :tags, :comments]) 61 | |> Repo.paginate(params) 62 | end) 63 | end 64 | 65 | conn 66 | |> assign(:pagination, pagination) 67 | |> render(:show) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /web/templates/user/comment/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
标签列表
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%= for comment <- @comments do %> 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | <% end %> 30 | 31 |
ID评论人评论内容文章标题操作
<%= comment.id %><%= comment.name %><%= comment.body %><%= article_name(comment) %> 25 | <%= link "Edit", to: user_comment_path(@conn, :edit, comment), class: "btn btn-xs btn-warning" %> 26 | <%= link "Delete", to: user_comment_path(@conn, :delete, comment), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-xs btn-danger" %> 27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | -------------------------------------------------------------------------------- /web/templates/session/home.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 | <%= link (Integer.to_string(@article_count) <> "篇文章"), to: user_article_path(@conn, :index) %> 10 |
11 |
12 |
13 |
14 | 15 | <%= link (Integer.to_string(@category_count) <> "个范畴"), to: user_category_path(@conn, :index) %> 16 |
17 |
18 |
19 |
20 | 21 | <%= link (Integer.to_string(@tag_count) <> "个标签"), to: user_tag_path(@conn, :index) %> 22 |
23 |
24 |
25 |
26 | 27 | <%= link (Integer.to_string(@comment_count) <> "条评论"), to: user_comment_path(@conn, :index) %> 28 |
29 |
30 |
31 |
32 | 33 | <%= link (Integer.to_string(@link_count) <> "个友链"), to: user_link_path(@conn, :index) %> 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /web/models/article.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Article do 2 | use HelloPhoenix.Web, :model 3 | 4 | schema "articles" do 5 | field :title, :string 6 | field :body, :string 7 | field :reading, :integer, default: 0 8 | field :hash_id, :string 9 | field :block, :boolean, default: false 10 | belongs_to :category, HelloPhoenix.Category, foreign_key: :category_id 11 | 12 | has_many :comments, HelloPhoenix.Comment, on_delete: :delete_all 13 | many_to_many :tags, HelloPhoenix.Tag, join_through: "posts_tags", on_delete: :delete_all 14 | 15 | timestamps() 16 | end 17 | 18 | @doc """ 19 | Builds a changeset based on the `struct` and `params`. 20 | """ 21 | def changeset(struct, params \\ %{}) do 22 | struct 23 | |> cast(params, [:title, :body, :reading, :category_id, :block]) 24 | |> validate_required([:title, :body, :category_id]) 25 | #|> strip_unsafe_body(params) 26 | |> validate_length(:title, min: 5) 27 | |> unique_constraint(:title) 28 | end 29 | 30 | @doc """ 31 | 添加hash之后的id 32 | """ 33 | def put_id_hash(%Ecto.Changeset{valid?: false} = changeset, _field1, _filed2) do 34 | changeset 35 | end 36 | 37 | def put_id_hash(%Ecto.Changeset{valid?: true} = changeset, field1, filed2) do 38 | value = get_field(changeset, field1) <> get_field(changeset, filed2) 39 | changeset |> put_change(:hash_id, :crypto.hash(:sha256, value) |> Base.encode16 |> String.downcase) 40 | end 41 | 42 | def create_changeset(struct, params \\ %{}) do 43 | struct 44 | |> cast(params, [:title, :body, :reading, :category_id, :block]) 45 | |> validate_required([:title, :body, :category_id]) 46 | #|> strip_unsafe_body(params) 47 | |> validate_length(:title, min: 5) 48 | |> unique_constraint(:title) 49 | |> put_id_hash(:title, :body) 50 | end 51 | 52 | defp strip_unsafe_body(model, %{"body" => nil}) do 53 | model 54 | end 55 | 56 | defp strip_unsafe_body(model, %{"body" => body}) do 57 | {:safe, clean_body} = Phoenix.HTML.html_escape(body) 58 | model |> put_change(:body, clean_body) 59 | end 60 | 61 | defp strip_unsafe_body(model, _) do 62 | model 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /web/templates/share/article_body.html.eex: -------------------------------------------------------------------------------- 1 | <%= for article <- @pagination do %> 2 |
3 | 4 |
5 |

6 | 7 | <%= article.title%> 8 | 9 |

10 | 26 |
27 | 28 |
29 |

30 |
31 | 32 |
33 | 阅读全文 34 |
35 | 36 |
37 |
38 | 39 | <%= for tag <- article.tags do %> 40 | <%= tag.name %> 41 | <% end %> 42 |
43 |
44 |
45 | <% end %> 46 | -------------------------------------------------------------------------------- /web/controllers/user/category_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.CategoryController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.Category 5 | 6 | def index(conn, _params) do 7 | categorys = Repo.all(Category) 8 | render(conn, "index.html", categorys: categorys) 9 | end 10 | 11 | def new(conn, _params) do 12 | changeset = Category.changeset(%Category{}) 13 | render(conn, "new.html", changeset: changeset) 14 | end 15 | 16 | def create(conn, %{"category" => category_params}) do 17 | changeset = Category.create_changeset(%Category{}, category_params) 18 | 19 | case Repo.insert(changeset) do 20 | {:ok, _category} -> 21 | conn 22 | |> put_flash(:info, "范畴创建成功。") 23 | |> redirect(to: user_category_path(conn, :index)) 24 | {:error, changeset} -> 25 | render(conn, "new.html", changeset: changeset) 26 | end 27 | end 28 | 29 | def show(conn, %{"id" => id}) do 30 | category = Repo.get!(Category, id) 31 | render(conn, "show.html", category: category) 32 | end 33 | 34 | def edit(conn, %{"id" => id}) do 35 | category = Repo.get!(Category, id) 36 | changeset = Category.changeset(category) 37 | render(conn, "edit.html", category: category, changeset: changeset) 38 | end 39 | 40 | def update(conn, %{"id" => id, "category" => category_params}) do 41 | category = Repo.get!(Category, id) 42 | changeset = Category.changeset(category, category_params) 43 | 44 | case Repo.update(changeset) do 45 | {:ok, _category} -> 46 | conn 47 | |> put_flash(:info, "范畴更新成功。") 48 | |> redirect(to: user_category_path(conn, :index)) 49 | {:error, changeset} -> 50 | render(conn, "edit.html", category: category, changeset: changeset) 51 | end 52 | end 53 | 54 | def delete(conn, %{"id" => id}) do 55 | category = Repo.get!(Category, id) 56 | 57 | # Here we use delete! (with a bang) because we expect 58 | # it to always work (and if it does not, it will raise). 59 | Repo.delete!(category) 60 | 61 | conn 62 | |> put_flash(:info, "范畴删除成功。") 63 | |> redirect(to: user_category_path(conn, :index)) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /web/controllers/user/comment_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.CommentController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.Comment 5 | 6 | def index(conn, _params) do 7 | comments = Comment 8 | |> Repo.all 9 | |> Repo.preload(:article) 10 | 11 | render(conn, "index.html", comments: comments) 12 | end 13 | 14 | def new(conn, _params) do 15 | changeset = Comment.changeset(%Comment{}) 16 | render(conn, "new.html", changeset: changeset) 17 | end 18 | 19 | def create(conn, %{"comment" => comment_params}) do 20 | changeset = Comment.changeset(%Comment{}, comment_params) 21 | 22 | case Repo.insert(changeset) do 23 | {:ok, _comment} -> 24 | conn 25 | |> put_flash(:info, "Comment created successfully.") 26 | |> redirect(to: user_comment_path(conn, :index)) 27 | {:error, changeset} -> 28 | render(conn, "new.html", changeset: changeset) 29 | end 30 | end 31 | 32 | def show(conn, %{"id" => id}) do 33 | comment = Repo.get!(Comment, id) 34 | render(conn, "show.html", comment: comment) 35 | end 36 | 37 | def edit(conn, %{"id" => id}) do 38 | comment = Repo.get!(Comment, id) 39 | changeset = Comment.changeset(comment) 40 | render(conn, "edit.html", comment: comment, changeset: changeset) 41 | end 42 | 43 | def update(conn, %{"id" => id, "comment" => comment_params}) do 44 | comment = Repo.get!(Comment, id) 45 | changeset = Comment.changeset(comment, comment_params) 46 | 47 | case Repo.update(changeset) do 48 | {:ok, _comment} -> 49 | conn 50 | |> put_flash(:info, "评论更新成功。") 51 | |> redirect(to: user_comment_path(conn, :index)) 52 | {:error, changeset} -> 53 | render(conn, "edit.html", comment: comment, changeset: changeset) 54 | end 55 | end 56 | 57 | def delete(conn, %{"id" => id}) do 58 | comment = Repo.get!(Comment, id) 59 | 60 | # Here we use delete! (with a bang) because we expect 61 | # it to always work (and if it does not, it will raise). 62 | Repo.delete!(comment) 63 | 64 | conn 65 | |> put_flash(:info, "评论删除成功。") 66 | |> redirect(to: user_comment_path(conn, :index)) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /web/controllers/user/upload_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.UploadController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{User} 5 | @allow_file_suffix [".gif", ".jpg", ".jpeg", ".png", ".svg"] 6 | import Ecto.Changeset, only: [add_error: 4] 7 | 8 | def new_avatar(conn, _params) do 9 | user = Repo.get!(User, get_session(conn, :user_id)) 10 | changeset = User.changeset(%User{}) 11 | 12 | conn 13 | |> assign(:changeset, changeset) 14 | |> assign(:avatar, user.avatar) 15 | |> render("photo_form.html") 16 | end 17 | 18 | def update_avatar(conn, %{"user" => user_params}) do 19 | user_id = get_session(conn, :user_id) 20 | if upload = user_params["avatar"] do 21 | extension = Path.extname(upload.filename) 22 | path = "priv/static/images/#{user_id}-profile#{extension}" 23 | 24 | changeset = User.changeset_avatar(%User{}) 25 | user = Repo.get!(User, get_session(conn, :user_id)) 26 | case extension in @allow_file_suffix do 27 | true -> 28 | case File.cp(upload.path, path) do 29 | :ok -> # 上传成功, 将图片地址入库 30 | User 31 | |> Repo.get!(user_id) 32 | |> User.changeset_avatar(%{"avatar" => "/images/#{user_id}-profile#{extension}"}) 33 | |> Repo.update! 34 | 35 | conn 36 | |> put_flash(:info, "修改头像成功") 37 | |> redirect(to: user_profile_path(conn, :show)) 38 | {:error, _} -> 39 | changeset = changeset |> add_error(:avatar, "上传出错", message: "上传出错") 40 | 41 | conn 42 | |> assign(:avatar, user.avatar) 43 | |> assign(:changeset, changeset) 44 | |> put_flash(:error, "修改头像失败") 45 | |> redirect(to: user_profile_path(conn, :show)) 46 | end 47 | false -> 48 | changeset = changeset |> add_error(:avatar, "不允许的后缀", message: "不允许的后缀") 49 | 50 | conn 51 | |> assign(:avatar, user.avatar) 52 | |> assign(:changeset, changeset) 53 | |> put_flash(:error, "修改头像失败") 54 | |> redirect(to: user_profile_path(conn, :show)) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :hello_phoenix, HelloPhoenix.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :hello_phoenix, HelloPhoenix.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :hello_phoenix, HelloPhoenix.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :hello_phoenix, HelloPhoenix.Endpoint, server: true 57 | # 58 | 59 | # Finally import the config/prod.secret.exs 60 | # which should be versioned separately. 61 | import_config "prod.secret.exs" 62 | -------------------------------------------------------------------------------- /web/templates/tag/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= render HelloPhoenix.ShareView, "article_body.html", pagination: @pagination, conn: @conn %> 8 | <%= pagination_links @conn, @pagination, [Map.get(@conn.params, "id")], path: &tag_path/4, action: :show %> 9 |
10 | 11 |
12 |
13 | <%= render HelloPhoenix.ShareView, "user_info.html" %> 14 | <%= render HelloPhoenix.ShareView, "hot_articles.html", hot_articles: @hot_articles, conn: @conn %> 15 | 16 | <%= render HelloPhoenix.ShareView, "category.html", categorys: @categorys, conn: @conn %> 17 | 18 |
19 |
标签
20 | 34 |
35 | 36 | <%= render HelloPhoenix.ShareView, "friend_ship.html", links: @links %> 37 |
38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /web/controllers/category_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CategoryController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{Category, Article, Tag, Link} 5 | 6 | defp common(conn) do 7 | categorys = ConCache.get_or_store(:hello_phoenix, "categorys", fn() -> 8 | Category 9 | |> Repo.all 10 | |> Repo.preload(:articles) 11 | end) 12 | 13 | tags = ConCache.get_or_store(:hello_phoenix, "tags", fn() -> 14 | Tag 15 | |> Repo.all 16 | |> Repo.preload(:articles) 17 | end) 18 | 19 | hot_articles = ConCache.get_or_store(:hello_phoenix, "hot_articles", fn() -> 20 | Article 21 | |> where(block: false) 22 | |> order_by(desc: :reading) 23 | |> limit(10) 24 | |> Repo.all 25 | |> Repo.preload(:category) 26 | end) 27 | 28 | links = ConCache.get_or_store(:hello_phoenix, "links", fn() -> 29 | Link 30 | |> Repo.all 31 | end) 32 | 33 | conn 34 | |> assign(:hot_articles, hot_articles) 35 | |> assign(:categorys, categorys) 36 | |> assign(:tags, tags) 37 | |> assign(:links, links) 38 | end 39 | 40 | def index(conn, params) do 41 | conn = common(conn) 42 | pagination = ConCache.get_or_store(:hello_phoenix, "pagination", fn() -> 43 | Article 44 | |> where(block: false) 45 | |> order_by(desc: :inserted_at) 46 | |> preload([:category, :tags, :comments]) 47 | |> Repo.paginate(params) 48 | end) 49 | 50 | conn 51 | |> assign(:pagination, pagination) 52 | |> render(:index) 53 | end 54 | 55 | def show(conn, params) do 56 | conn = common(conn) 57 | pagination = case Map.fetch(params, "id") do 58 | {:ok, id} -> 59 | category = Category |> where(hash_id: ^id) |> Repo.one! 60 | Article 61 | |> where(block: false) 62 | |> where(category_id: ^category.id) 63 | |> order_by(desc: :inserted_at) 64 | |> preload([:category, :tags, :comments]) 65 | |> Repo.paginate(params) 66 | {:error} -> 67 | Article 68 | |> where(block: false) 69 | |> order_by(desc: :inserted_at) 70 | |> preload([:category, :tags, :comments]) 71 | |> Repo.paginate(params) 72 | end 73 | 74 | conn 75 | |> assign(:pagination, pagination) 76 | |> render(:show) 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.SessionController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{User, Article, Link, Category, Comment, Tag} 5 | 6 | def new(conn, _params) do 7 | changeset = User.changeset(%User{}) 8 | conn 9 | |> assign(:changeset, changeset) 10 | |> render("new.html") 11 | end 12 | 13 | def create(conn, %{"user" => user_params}) do 14 | user = Repo.get_by(User, name: user_params["name"]) 15 | changeset = User.changeset_login(%User{}, user_params) 16 | 17 | #checkd = case user do 18 | # nil -> false # 用户不存在 19 | # _ -> case checkpw(password, user.password) do 20 | # true -> true 21 | # _ -> false # 密码错误 22 | # end 23 | # end 24 | 25 | case changeset.valid? do 26 | true -> 27 | conn 28 | |> put_flash(:info, "登陆成功") 29 | |> put_session(:user_id, user.id) 30 | |> put_session(:user_avatar, user.avatar) 31 | |> put_session(:username, user.name) 32 | |> redirect(to: user_session_path(conn, :home)) 33 | false -> 34 | conn 35 | |> assign(:changeset, changeset) 36 | |> put_flash(:error, "登陆失败,用户或者密码错误。") 37 | |> render("new.html") 38 | end 39 | end 40 | 41 | def home(conn, _params) do 42 | user = Repo.get!(User, get_session(conn, :user_id)) 43 | article_count = from(a in Article, select: count(a.id)) |> Repo.one 44 | tag_count = from(t in Tag, select: count(t.id)) |> Repo.one 45 | link_count = from(l in Link, select: count(l.id)) |> Repo.one 46 | comment_count = from(cm in Comment, select: count(cm.id)) |> Repo.one 47 | category_count = from(cg in Category, select: count(cg.id)) |> Repo.one 48 | 49 | conn 50 | |> assign(:username, get_session(conn, :username)) 51 | |> assign(:avatar, user.avatar) 52 | |> assign(:article_count, article_count) 53 | |> assign(:link_count, link_count) 54 | |> assign(:tag_count, tag_count) 55 | |> assign(:category_count, category_count) 56 | |> assign(:comment_count, comment_count) 57 | |> render("home.html") 58 | end 59 | 60 | def delete(conn, _params) do 61 | conn 62 | |> delete_session(:user_id) 63 | |> delete_session(:username) 64 | |> delete_session(:user_avatar) 65 | |> put_flash(:info, "退出成功。") 66 | |> redirect(to: session_path(conn, :new)) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /web/templates/user/article/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 |
文章列表
8 |
9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <%= for article <- @articles do %> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | <% end %> 36 | 37 |
ID标题点击次数所属范畴Block操作
<%= article.id %><%= article.title %><%= article.reading %><%= category_name(article) %><%= article.block %> 30 | <%= link "Show", to: user_article_path(@conn, :show, article), class: "btn btn-info btn-xs" %> 31 | <%= link "Edit", to: user_article_path(@conn, :edit, article), class: "btn btn-warning btn-xs" %> 32 | <%= link "Delete", to: user_article_path(@conn, :delete, article), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %> 33 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_format/3 26 | msgid "has invalid format" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_subset/3 30 | msgid "has an invalid entry" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_exclusion/3 34 | msgid "is reserved" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_confirmation/3 38 | msgid "does not match confirmation" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.no_assoc_constraint/3 42 | msgid "is still associated to this entry" 43 | msgstr "" 44 | 45 | msgid "are still associated to this entry" 46 | msgstr "" 47 | 48 | ## From Ecto.Changeset.validate_length/3 49 | msgid "should be %{count} character(s)" 50 | msgid_plural "should be %{count} character(s)" 51 | msgstr[0] "" 52 | msgstr[1] "" 53 | 54 | msgid "should have %{count} item(s)" 55 | msgid_plural "should have %{count} item(s)" 56 | msgstr[0] "" 57 | msgstr[1] "" 58 | 59 | msgid "should be at least %{count} character(s)" 60 | msgid_plural "should be at least %{count} character(s)" 61 | msgstr[0] "" 62 | msgstr[1] "" 63 | 64 | msgid "should have at least %{count} item(s)" 65 | msgid_plural "should have at least %{count} item(s)" 66 | msgstr[0] "" 67 | msgstr[1] "" 68 | 69 | msgid "should be at most %{count} character(s)" 70 | msgid_plural "should be at most %{count} character(s)" 71 | msgstr[0] "" 72 | msgstr[1] "" 73 | 74 | msgid "should have at most %{count} item(s)" 75 | msgid_plural "should have at most %{count} item(s)" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | ## From Ecto.Changeset.validate_number/3 80 | msgid "must be less than %{number}" 81 | msgstr "" 82 | 83 | msgid "must be greater than %{number}" 84 | msgstr "" 85 | 86 | msgid "must be less than or equal to %{number}" 87 | msgstr "" 88 | 89 | msgid "must be greater than or equal to %{number}" 90 | msgstr "" 91 | 92 | msgid "must be equal to %{number}" 93 | msgstr "" 94 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_format/3 24 | msgid "has invalid format" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_subset/3 28 | msgid "has an invalid entry" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_exclusion/3 32 | msgid "is reserved" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_confirmation/3 36 | msgid "does not match confirmation" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.no_assoc_constraint/3 40 | msgid "is still associated to this entry" 41 | msgstr "" 42 | 43 | msgid "are still associated to this entry" 44 | msgstr "" 45 | 46 | ## From Ecto.Changeset.validate_length/3 47 | msgid "should be %{count} character(s)" 48 | msgid_plural "should be %{count} character(s)" 49 | msgstr[0] "" 50 | msgstr[1] "" 51 | 52 | msgid "should have %{count} item(s)" 53 | msgid_plural "should have %{count} item(s)" 54 | msgstr[0] "" 55 | msgstr[1] "" 56 | 57 | msgid "should be at least %{count} character(s)" 58 | msgid_plural "should be at least %{count} character(s)" 59 | msgstr[0] "" 60 | msgstr[1] "" 61 | 62 | msgid "should have at least %{count} item(s)" 63 | msgid_plural "should have at least %{count} item(s)" 64 | msgstr[0] "" 65 | msgstr[1] "" 66 | 67 | msgid "should be at most %{count} character(s)" 68 | msgid_plural "should be at most %{count} character(s)" 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | msgid "should have at most %{count} item(s)" 73 | msgid_plural "should have at most %{count} item(s)" 74 | msgstr[0] "" 75 | msgstr[1] "" 76 | 77 | ## From Ecto.Changeset.validate_number/3 78 | msgid "must be less than %{number}" 79 | msgstr "" 80 | 81 | msgid "must be greater than %{number}" 82 | msgstr "" 83 | 84 | msgid "must be less than or equal to %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than or equal to %{number}" 88 | msgstr "" 89 | 90 | msgid "must be equal to %{number}" 91 | msgstr "" 92 | -------------------------------------------------------------------------------- /test/controllers/link_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.LinkControllerTest do 2 | use HelloPhoenix.ConnCase 3 | 4 | alias HelloPhoenix.Link 5 | @valid_attrs %{name: "some content", url: "some content"} 6 | @invalid_attrs %{} 7 | 8 | test "lists all entries on index", %{conn: conn} do 9 | conn = get conn, link_path(conn, :index) 10 | assert html_response(conn, 200) =~ "Listing users/links" 11 | end 12 | 13 | test "renders form for new resources", %{conn: conn} do 14 | conn = get conn, link_path(conn, :new) 15 | assert html_response(conn, 200) =~ "New link" 16 | end 17 | 18 | test "creates resource and redirects when data is valid", %{conn: conn} do 19 | conn = post conn, link_path(conn, :create), link: @valid_attrs 20 | assert redirected_to(conn) == link_path(conn, :index) 21 | assert Repo.get_by(Link, @valid_attrs) 22 | end 23 | 24 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 25 | conn = post conn, link_path(conn, :create), link: @invalid_attrs 26 | assert html_response(conn, 200) =~ "New link" 27 | end 28 | 29 | test "shows chosen resource", %{conn: conn} do 30 | link = Repo.insert! %Link{} 31 | conn = get conn, link_path(conn, :show, link) 32 | assert html_response(conn, 200) =~ "Show link" 33 | end 34 | 35 | test "renders page not found when id is nonexistent", %{conn: conn} do 36 | assert_error_sent 404, fn -> 37 | get conn, link_path(conn, :show, -1) 38 | end 39 | end 40 | 41 | test "renders form for editing chosen resource", %{conn: conn} do 42 | link = Repo.insert! %Link{} 43 | conn = get conn, link_path(conn, :edit, link) 44 | assert html_response(conn, 200) =~ "Edit link" 45 | end 46 | 47 | test "updates chosen resource and redirects when data is valid", %{conn: conn} do 48 | link = Repo.insert! %Link{} 49 | conn = put conn, link_path(conn, :update, link), link: @valid_attrs 50 | assert redirected_to(conn) == link_path(conn, :show, link) 51 | assert Repo.get_by(Link, @valid_attrs) 52 | end 53 | 54 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 55 | link = Repo.insert! %Link{} 56 | conn = put conn, link_path(conn, :update, link), link: @invalid_attrs 57 | assert html_response(conn, 200) =~ "Edit link" 58 | end 59 | 60 | test "deletes chosen resource", %{conn: conn} do 61 | link = Repo.insert! %Link{} 62 | conn = delete conn, link_path(conn, :delete, link) 63 | assert redirected_to(conn) == link_path(conn, :index) 64 | refute Repo.get(Link, link.id) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserControllerTest do 2 | use HelloPhoenix.ConnCase 3 | 4 | alias HelloPhoenix.User 5 | @valid_attrs %{bio: "some content", email: "some content", name: "some content", number_of_pets: 42} 6 | @invalid_attrs %{} 7 | 8 | test "lists all entries on index", %{conn: conn} do 9 | conn = get conn, user_path(conn, :index) 10 | assert html_response(conn, 200) =~ "Listing users" 11 | end 12 | 13 | test "renders form for new resources", %{conn: conn} do 14 | conn = get conn, user_path(conn, :new) 15 | assert html_response(conn, 200) =~ "New user" 16 | end 17 | 18 | test "creates resource and redirects when data is valid", %{conn: conn} do 19 | conn = post conn, user_path(conn, :create), user: @valid_attrs 20 | assert redirected_to(conn) == user_path(conn, :index) 21 | assert Repo.get_by(User, @valid_attrs) 22 | end 23 | 24 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 25 | conn = post conn, user_path(conn, :create), user: @invalid_attrs 26 | assert html_response(conn, 200) =~ "New user" 27 | end 28 | 29 | test "shows chosen resource", %{conn: conn} do 30 | user = Repo.insert! %User{} 31 | conn = get conn, user_path(conn, :show, user) 32 | assert html_response(conn, 200) =~ "Show user" 33 | end 34 | 35 | test "renders page not found when id is nonexistent", %{conn: conn} do 36 | assert_error_sent 404, fn -> 37 | get conn, user_path(conn, :show, -1) 38 | end 39 | end 40 | 41 | test "renders form for editing chosen resource", %{conn: conn} do 42 | user = Repo.insert! %User{} 43 | conn = get conn, user_path(conn, :edit, user) 44 | assert html_response(conn, 200) =~ "Edit user" 45 | end 46 | 47 | test "updates chosen resource and redirects when data is valid", %{conn: conn} do 48 | user = Repo.insert! %User{} 49 | conn = put conn, user_path(conn, :update, user), user: @valid_attrs 50 | assert redirected_to(conn) == user_path(conn, :show, user) 51 | assert Repo.get_by(User, @valid_attrs) 52 | end 53 | 54 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 55 | user = Repo.insert! %User{} 56 | conn = put conn, user_path(conn, :update, user), user: @invalid_attrs 57 | assert html_response(conn, 200) =~ "Edit user" 58 | end 59 | 60 | test "deletes chosen resource", %{conn: conn} do 61 | user = Repo.insert! %User{} 62 | conn = delete conn, user_path(conn, :delete, user) 63 | assert redirected_to(conn) == user_path(conn, :index) 64 | refute Repo.get(User, user.id) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/controllers/article_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ArticleControllerTest do 2 | use HelloPhoenix.ConnCase 3 | 4 | alias HelloPhoenix.Article 5 | @valid_attrs %{body: "some content", title: "some content"} 6 | @invalid_attrs %{} 7 | 8 | test "lists all entries on index", %{conn: conn} do 9 | conn = get conn, article_path(conn, :index) 10 | assert html_response(conn, 200) =~ "Listing articles" 11 | end 12 | 13 | test "renders form for new resources", %{conn: conn} do 14 | conn = get conn, article_path(conn, :new) 15 | assert html_response(conn, 200) =~ "New article" 16 | end 17 | 18 | test "creates resource and redirects when data is valid", %{conn: conn} do 19 | conn = post conn, article_path(conn, :create), article: @valid_attrs 20 | assert redirected_to(conn) == article_path(conn, :index) 21 | assert Repo.get_by(Article, @valid_attrs) 22 | end 23 | 24 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 25 | conn = post conn, article_path(conn, :create), article: @invalid_attrs 26 | assert html_response(conn, 200) =~ "New article" 27 | end 28 | 29 | test "shows chosen resource", %{conn: conn} do 30 | article = Repo.insert! %Article{} 31 | conn = get conn, article_path(conn, :show, article) 32 | assert html_response(conn, 200) =~ "Show article" 33 | end 34 | 35 | test "renders page not found when id is nonexistent", %{conn: conn} do 36 | assert_error_sent 404, fn -> 37 | get conn, article_path(conn, :show, -1) 38 | end 39 | end 40 | 41 | test "renders form for editing chosen resource", %{conn: conn} do 42 | article = Repo.insert! %Article{} 43 | conn = get conn, article_path(conn, :edit, article) 44 | assert html_response(conn, 200) =~ "Edit article" 45 | end 46 | 47 | test "updates chosen resource and redirects when data is valid", %{conn: conn} do 48 | article = Repo.insert! %Article{} 49 | conn = put conn, article_path(conn, :update, article), article: @valid_attrs 50 | assert redirected_to(conn) == article_path(conn, :show, article) 51 | assert Repo.get_by(Article, @valid_attrs) 52 | end 53 | 54 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 55 | article = Repo.insert! %Article{} 56 | conn = put conn, article_path(conn, :update, article), article: @invalid_attrs 57 | assert html_response(conn, 200) =~ "Edit article" 58 | end 59 | 60 | test "deletes chosen resource", %{conn: conn} do 61 | article = Repo.insert! %Article{} 62 | conn = delete conn, article_path(conn, :delete, article) 63 | assert redirected_to(conn) == article_path(conn, :index) 64 | refute Repo.get(Article, article.id) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /web/templates/user/article/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 6 |
7 |
8 |
9 |
<%= @article.title %>
10 | 35 |
36 |
37 |
38 | <%= markdown @article.body %> 39 |
40 |
41 | 42 | <%= for tag <- @article.tags do %> 43 | <%= tag.name %> 44 | <% end %> 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /test/controllers/category_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CategoryControllerTest do 2 | use HelloPhoenix.ConnCase 3 | 4 | alias HelloPhoenix.Category 5 | @valid_attrs %{name: "some content"} 6 | @invalid_attrs %{} 7 | 8 | test "lists all entries on index", %{conn: conn} do 9 | conn = get conn, category_path(conn, :index) 10 | assert html_response(conn, 200) =~ "Listing categorys" 11 | end 12 | 13 | test "renders form for new resources", %{conn: conn} do 14 | conn = get conn, category_path(conn, :new) 15 | assert html_response(conn, 200) =~ "New category" 16 | end 17 | 18 | test "creates resource and redirects when data is valid", %{conn: conn} do 19 | conn = post conn, category_path(conn, :create), category: @valid_attrs 20 | assert redirected_to(conn) == category_path(conn, :index) 21 | assert Repo.get_by(Category, @valid_attrs) 22 | end 23 | 24 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 25 | conn = post conn, category_path(conn, :create), category: @invalid_attrs 26 | assert html_response(conn, 200) =~ "New category" 27 | end 28 | 29 | test "shows chosen resource", %{conn: conn} do 30 | category = Repo.insert! %Category{} 31 | conn = get conn, category_path(conn, :show, category) 32 | assert html_response(conn, 200) =~ "Show category" 33 | end 34 | 35 | test "renders page not found when id is nonexistent", %{conn: conn} do 36 | assert_error_sent 404, fn -> 37 | get conn, category_path(conn, :show, -1) 38 | end 39 | end 40 | 41 | test "renders form for editing chosen resource", %{conn: conn} do 42 | category = Repo.insert! %Category{} 43 | conn = get conn, category_path(conn, :edit, category) 44 | assert html_response(conn, 200) =~ "Edit category" 45 | end 46 | 47 | test "updates chosen resource and redirects when data is valid", %{conn: conn} do 48 | category = Repo.insert! %Category{} 49 | conn = put conn, category_path(conn, :update, category), category: @valid_attrs 50 | assert redirected_to(conn) == category_path(conn, :show, category) 51 | assert Repo.get_by(Category, @valid_attrs) 52 | end 53 | 54 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 55 | category = Repo.insert! %Category{} 56 | conn = put conn, category_path(conn, :update, category), category: @invalid_attrs 57 | assert html_response(conn, 200) =~ "Edit category" 58 | end 59 | 60 | test "deletes chosen resource", %{conn: conn} do 61 | category = Repo.insert! %Category{} 62 | conn = delete conn, category_path(conn, :delete, category) 63 | assert redirected_to(conn) == category_path(conn, :index) 64 | refute Repo.get(Category, category.id) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Router do 2 | use HelloPhoenix.Web, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | # pipeline :browser_no_csrf do 13 | # plug :accepts, ["html"] 14 | # plug :fetch_session 15 | # plug :fetch_flash 16 | # plug :put_secure_browser_headers 17 | # end 18 | 19 | pipeline :session do 20 | plug HelloPhoenix.Plugs.Session 21 | end 22 | 23 | pipeline :api do 24 | plug :accepts, ["json"] 25 | end 26 | 27 | scope "/users", HelloPhoenix, as: :user do 28 | pipe_through [:browser, :session] 29 | 30 | get "/password/reset", UserController, :edit 31 | put "/password/reset", UserController, :update 32 | 33 | get "/", SessionController, :home 34 | get "/home", SessionController, :home 35 | 36 | get "/settings/profile", User.ProfileController, :show 37 | get "/upload_avatar", User.UploadController, :new_avatar 38 | post "/upload_avatar", User.UploadController, :update_avatar 39 | resources "/categorys", User.CategoryController 40 | resources "/articles", User.ArticleController 41 | resources "/comments", User.CommentController 42 | resources "/tags", User.TagController 43 | resources "/links", User.LinkController 44 | end 45 | 46 | scope "/", HelloPhoenix do 47 | pipe_through :browser # Use the default browser stack 48 | 49 | get "/achieve", AchieveController, :index 50 | get "/projects", ProjectController, :index 51 | get "/search", SearchController, :index 52 | get "/about", AboutController, :index 53 | 54 | get "/users/index", SessionController, :index 55 | get "/users/login", SessionController, :new 56 | get "/users/logout", SessionController, :delete 57 | post "/users/login", SessionController, :create 58 | get "/users/register", UserController, :new 59 | post "/users/register", UserController, :create 60 | resources "/tags", TagController, only: [:show] 61 | resources "/categorys", CategoryController do 62 | resources "/articles", ArticleController do 63 | resources "/comments", CommentController, only: [:create] 64 | end 65 | end 66 | get "/", CategoryController, :index 67 | end 68 | 69 | # scope "/", HelloPhoenix do 70 | # pipe_through :browser_no_csrf # Use the default browser stack 71 | # end 72 | # scope "/admin", HelloPhoenix.Admin, as: :admin do 73 | # pipe_through :browser 74 | # 75 | # resources "/reviews", ReviewController 76 | # resources "/users", UserController 77 | #k end 78 | 79 | # Other scopes may use custom stacks. 80 | # scope "/api", HelloPhoenix do 81 | # pipe_through :api 82 | # end 83 | end 84 | -------------------------------------------------------------------------------- /web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User do 2 | use HelloPhoenix.Web, :model 3 | 4 | alias HelloPhoenix.Repo 5 | 6 | import Comeonin.Bcrypt, only: [hashpwsalt: 1, checkpw: 2] 7 | 8 | schema "users" do 9 | field :name, :string 10 | field :password, :string 11 | field :avatar, :string 12 | 13 | field :old_password, :string, virtual: true 14 | field :password_confirmation, :string, virtual: true 15 | timestamps() 16 | end 17 | 18 | @doc """ 19 | Builds a changeset based on the `struct` and `params`. 20 | """ 21 | def changeset(struct, params \\ %{}) do 22 | struct 23 | |> cast(params, [:name, :password]) 24 | |> validate_required([:name, :password]) 25 | |> unique_constraint(:name) 26 | end 27 | 28 | def changeset_avatar(struct, params \\ %{}) do 29 | struct 30 | |> cast(params, [:avatar]) 31 | |> validate_required([:avatar]) 32 | end 33 | 34 | @doc """ 35 | 将密码加密 36 | """ 37 | def put_password_hash(%Ecto.Changeset{valid?: false} = changeset, _field) do 38 | changeset 39 | end 40 | 41 | def put_password_hash(%Ecto.Changeset{valid?: true} = changeset, field) do 42 | value = get_field(changeset, field) 43 | 44 | changeset |> put_change(:password, hashpwsalt(value)) 45 | end 46 | 47 | @doc """ 48 | 验证用户是否存在 49 | """ 50 | def validate_user_exist(%Ecto.Changeset{valid?: false} = changeset, _field) do 51 | changeset 52 | end 53 | 54 | def validate_user_exist(%Ecto.Changeset{valid?: true} = changeset, field) do 55 | value = get_field(changeset, field) 56 | 57 | user = __MODULE__ |> Repo.get_by(name: value) 58 | 59 | case user do 60 | nil -> 61 | changeset 62 | |> add_error(field, "用户不存在") 63 | user -> 64 | changeset 65 | |> put_change(:user, user) 66 | end 67 | end 68 | 69 | @doc """ 70 | 验证密码是否正确 71 | """ 72 | def validate_password(%Ecto.Changeset{valid?: false} = changeset, _field) do 73 | changeset 74 | end 75 | 76 | def validate_password(%Ecto.Changeset{valid?: true} = changeset, field) do 77 | user = get_field(changeset, :user) || changeset.data 78 | password = get_field(changeset, field) 79 | 80 | case check_password?(password, user.password) do 81 | true -> 82 | changeset 83 | false -> 84 | changeset 85 | |> add_error(field, "密码错误") 86 | end 87 | end 88 | 89 | def check_password?(password, password_hash) do 90 | checkpw(password, password_hash) 91 | end 92 | 93 | def changeset_register(struct, params \\ %{}) do 94 | struct 95 | |> cast(params, [:name, :password, :password_confirmation]) 96 | |> validate_required([:name, :password, :password_confirmation], message: "字段不能为空") 97 | |> validate_length(:password, min: 6, message: "密码长度不能小于6位") 98 | |> validate_confirmation(:password, message: "两次密码输入不一致") 99 | |> unique_constraint(:name, message: "用户名已经被注册") 100 | |> put_password_hash(:password) 101 | end 102 | 103 | def changeset_reset_password(struct, params \\ %{}) do 104 | struct 105 | |> cast(params, [:old_password, :password, :password_confirmation]) 106 | |> validate_required([:old_password, :password, :password_confirmation], message: "字段不能为空") 107 | |> validate_length(:password, min: 6, message: "新密码长度不能小于6位") 108 | |> validate_confirmation(:password, message: "两次密码输入不一致") 109 | |> validate_password(:old_password) 110 | |> put_password_hash(:password) 111 | end 112 | 113 | def changeset_login(struct, params \\ %{}) do 114 | struct 115 | |> cast(params, [:name, :password]) 116 | |> validate_required([:name, :password]) 117 | |> validate_user_exist(:name) 118 | |> validate_password(:password) 119 | end 120 | 121 | end 122 | -------------------------------------------------------------------------------- /web/templates/user/profile/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render HelloPhoenix.ShareView, "user_left_nagiva.html", conn: @conn %> 5 |
6 |
7 | <%= form_for @changeset, user_upload_path(@conn, :update_avatar), [multipart: true], fn f -> %> 8 | <%= if @changeset.action do %> 9 |
10 |

Oops, something went wrong! Please check the errors below.

11 |
12 | <% end %> 13 | 14 |
15 | 更改头像 16 | 禁止使用任何低俗或者敏感图片作为头像 17 |
18 |
19 |
20 |

Profile picture

21 | > 22 | > 23 | > 24 | > 25 | <%= file_input f, :avatar, class: "file", id: "upload_button" %> 26 |
27 | 28 |
29 | <%= submit "Update Profile", class: "btn btn-sm btn-primary", id: "update_profile" %> 30 |
31 | <% end %> 32 |
33 | 34 |
35 | <%= form_for @passwd_changeset, user_user_path(@conn, :update), fn f -> %> 36 | <%= if @passwd_changeset.action do %> 37 |
38 |

Oops, something went wrong! Please check the errors below.

39 |
40 | <% end %> 41 |
42 | 更改密码 43 | 如果你不打算更改密码,请留空以下区域 44 |
45 |
46 |
47 | <%= label f, :old_password, class: "control-label" %> 48 | <%= password_input f, :old_password, class: "form-control" %> 49 | <%= error_tag f, :old_password %> 50 |
51 | 52 |
53 | <%= label f, :password, class: "control-label" %> 54 | <%= password_input f, :password, class: "form-control" %> 55 | <%= error_tag f, :password %> 56 |
57 | 58 |
59 | <%= label f, :password_confirmation, class: "control-label" %> 60 | <%= password_input f, :password_confirmation, class: "form-control" %> 61 | <%= error_tag f, :password_confirmation %> 62 |
63 | 64 |
65 | <%= submit "Update Password", class: "btn btn-sm btn-primary", id: "update_profile" %> 66 |
67 | <% end %> 68 |
69 |
70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"comeonin": {:hex, :comeonin, "2.6.0", "74c288338b33205f9ce97e2117bb9a2aaab103a1811d243443d76fdb62f904ac", [:mix, :make, :make], []}, 2 | "con_cache": {:hex, :con_cache, "0.11.1", "acbe5a1d10c47faba30d9629c9121e1ef9bca0aa5eddbb86f4e777bd9f9f9b6f", [:mix], [{:exactor, "~> 2.2.0", [hex: :exactor, optional: false]}]}, 3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 4 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, 5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 6 | "db_connection": {:hex, :db_connection, "1.0.0", "63c03e520d54886a66104d34e32397ba960db6e74b596ce221592c07d6a40d8d", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 7 | "decimal": {:hex, :decimal, "1.2.0", "462960fd71af282e570f7b477f6be56bf8968e68277d4d0b641a635269bf4b0d", [:mix], []}, 8 | "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 9 | "ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]}, 10 | "exactor": {:hex, :exactor, "2.2.2", "90b27d72c05614801a60f8400afd4e4346dfc33ea9beffe3b98a794891d2ff96", [:mix], []}, 11 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, 12 | "gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []}, 13 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []}, 14 | "phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 15 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 16 | "phoenix_html": {:hex, :phoenix_html, "2.7.0", "19e12e2044340c2e43df206a06d059677c59ea1868bd1c35165438d592cd420b", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 17 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, 18 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []}, 19 | "plug": {:hex, :plug, "1.2.2", "cfbda521b54c92ab8ddffb173fbaabed8d8fc94bec07cd9bb58a84c1c501b0bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 20 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 21 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 22 | "postgrex": {:hex, :postgrex, "0.12.1", "2f8b46cb3a44dcd42f42938abedbfffe7e103ba4ce810ccbeee8dcf27ca0fb06", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, 23 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}, 24 | "scrivener": {:hex, :scrivener, "2.1.1", "eb52c8b7d283e8999edd6fd50d872ab870669d1f4504134841d0845af11b5ef3", [:mix], []}, 25 | "scrivener_ecto": {:hex, :scrivener_ecto, "1.0.2", "4b10a2e6c23ed8aae59731d7ae71bfd55afea6559aae61b124e6e521055b4a9c", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:postgrex, "~> 0.11.0 or ~> 0.12.0", [hex: :postgrex, optional: true]}, {:scrivener, "~> 2.0", [hex: :scrivener, optional: false]}]}, 26 | "scrivener_html": {:hex, :scrivener_html, "1.3.3", "83174a770957899e18b0e760bcacb841a91ba58c81393740613ead56728bd0cb", [:mix], [{:phoenix, "~> 1.0 or ~> 1.2", [hex: :phoenix, optional: true]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, optional: false]}, {:scrivener, "~> 1.2 or ~> 2.0", [hex: :scrivener, optional: false]}]}} 27 | -------------------------------------------------------------------------------- /web/controllers/user/article_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User.ArticleController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{ArticleTag, Article, Category, Tag, Comment} 5 | 6 | def index(conn, _params) do 7 | articles = Article 8 | |> order_by(desc: :id) 9 | |> Repo.all 10 | |> Repo.preload(:category) 11 | 12 | render(conn, "index.html", articles: articles) 13 | end 14 | 15 | def new(conn, _params) do 16 | categorys = Repo.all(Category) 17 | tags = Repo.all(Tag) 18 | 19 | changeset = Article.changeset(%Article{}) 20 | 21 | conn 22 | |> assign(:tags, tags) 23 | |> assign(:exist_tags, []) 24 | |> assign(:categorys, categorys) 25 | |> assign(:changeset, changeset) 26 | |> render("new.html") 27 | end 28 | 29 | def create(conn, %{"article" => article_params, "tags" => tag_ids}) do 30 | changeset = Article.create_changeset(%Article{}, article_params) 31 | 32 | case Repo.insert(changeset) do 33 | {:ok, article} -> 34 | article 35 | |> Repo.preload(:tags) 36 | |> Ecto.Changeset.change() 37 | |> Ecto.Changeset.put_assoc(:tags, Enum.map(tag_ids, fn id -> Repo.get(Tag, id) end)) 38 | |> Repo.update! 39 | 40 | conn 41 | |> put_flash(:info, "文章创建成功。") 42 | |> redirect(to: user_article_path(conn, :index)) 43 | {:error, changeset} -> 44 | conn 45 | |> assign(:exist_tags, []) 46 | |> assign(:tags, Repo.all(Tag)) 47 | |> assign(:categorys, Repo.all(Category)) 48 | |> assign(:changeset, changeset) 49 | |> render("new.html") 50 | end 51 | end 52 | 53 | def create(conn, %{"article" => article_params}) do 54 | changeset = Article.create_changeset(%Article{}, article_params) 55 | 56 | case Repo.insert(changeset) do 57 | {:ok, _article} -> 58 | 59 | conn 60 | |> put_flash(:info, "文章创建成功。") 61 | |> redirect(to: user_article_path(conn, :index)) 62 | {:error, changeset} -> 63 | conn 64 | |> assign(:exist_tags, []) 65 | |> assign(:tags, Repo.all(Tag)) 66 | |> assign(:categorys, Repo.all(Category)) 67 | |> assign(:changeset, changeset) 68 | |> render("new.html") 69 | end 70 | end 71 | 72 | 73 | def show(conn, %{"id" => id}) do 74 | #article = Repo.get!(Article, id) 75 | #render(conn, "show.html", article: article) 76 | article = Article 77 | |> Repo.get!(id) 78 | |> Repo.preload([:category, :tags, :comments]) 79 | 80 | comments = Comment 81 | |> where(article_id: ^article.id) 82 | |> order_by(asc: :inserted_at) 83 | |> Repo.all 84 | changeset = Comment.changeset(%Comment{}) 85 | conn 86 | |> assign(:article, article) 87 | |> assign(:comments, comments) 88 | |> assign(:changeset, changeset) 89 | |> assign(:category_id, article.category.id) 90 | |> render("show.html") 91 | end 92 | 93 | def edit(conn, %{"id" => id}) do 94 | article = Repo.get!(Article, id) |> Repo.preload(:tags) 95 | exist_tags = article.tags 96 | tags = Repo.all(Tag) 97 | categorys = Repo.all(Category) 98 | changeset = Article.changeset(article) 99 | 100 | conn 101 | |> assign(:tags, tags) 102 | |> assign(:exist_tags, exist_tags) 103 | |> assign(:article, article) 104 | |> assign(:changeset, changeset) 105 | |> assign(:categorys, categorys) 106 | |> render("edit.html") 107 | end 108 | 109 | def update(conn, %{"id" => id, "article" => article_params, "tags" => tag_ids}) do 110 | article = Repo.get!(Article, id) 111 | changeset = Article.changeset(article, article_params) 112 | 113 | case Repo.update(changeset) do 114 | {:ok, article} -> 115 | from(p in ArticleTag, where: p.article_id == ^id) |> Repo.delete_all # 删除之前所有关联 116 | 117 | article # 重新插入所有关联 118 | |> Repo.preload(:tags) 119 | |> Ecto.Changeset.change() 120 | |> Ecto.Changeset.put_assoc(:tags, Enum.map(tag_ids, fn id -> Repo.get(Tag, id) end)) 121 | |> Repo.update! 122 | 123 | conn 124 | |> put_flash(:info, "文章更新成功。") 125 | |> redirect(to: user_article_path(conn, :show, article)) 126 | {:error, changeset} -> 127 | conn 128 | |> assign(:exist_tags, []) 129 | |> assign(:article, article) 130 | |> assign(:tags, Repo.all(Tag)) 131 | |> assign(:changeset, changeset) 132 | |> assign(:categorys, Repo.all(Category)) 133 | |> render("edit.html") 134 | end 135 | end 136 | 137 | def update(conn, %{"id" => id, "article" => article_params}) do 138 | article = Repo.get!(Article, id) 139 | changeset = Article.changeset(article, article_params) 140 | 141 | case Repo.update(changeset) do 142 | {:ok, article} -> 143 | from(p in ArticleTag, where: p.article_id == ^id) |> Repo.delete_all # 删除之前所有关联 144 | 145 | conn 146 | |> put_flash(:info, "文章更新成功。") 147 | |> redirect(to: user_article_path(conn, :show, article)) 148 | {:error, changeset} -> 149 | conn 150 | |> assign(:exist_tags, []) 151 | |> assign(:article, article) 152 | |> assign(:tags, Repo.all(Tag)) 153 | |> assign(:changeset, changeset) 154 | |> assign(:categorys, Repo.all(Category)) 155 | |> render("edit.html") 156 | end 157 | end 158 | 159 | 160 | def delete(conn, %{"id" => id}) do 161 | article = Repo.get!(Article, id) 162 | 163 | # Here we use delete! (with a bang) because we expect 164 | # it to always work (and if it does not, it will raise). 165 | Repo.delete!(article) 166 | 167 | conn 168 | |> put_flash(:info, "文章删除成功。") 169 | |> redirect(to: user_article_path(conn, :index)) 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /web/templates/article/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
<%= @article.title %>
7 | 32 |
33 |
34 |
35 | <%= markdown @article.body %> 36 |
37 |
38 | 39 | <%= for tag <- @article.tags do %> 40 | <%= tag.name %> 41 | <% end %> 42 |
43 |
44 |
45 | 自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) 47 |
48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 |
评论
57 |
58 |
59 | <%= if @comments == [] do %> 60 |

暂无评论~~

61 | <% end %> 62 | <%= for comment <- @comments do %> 63 |
64 |
65 | 66 | 67 | 68 |
69 |
70 |
71 | 72 | <%= comment.name %> 73 | 74 | 75 | 76 | 77 |
78 |
79 | <%= comment.inserted_at %> 80 |
81 |
82 |

<%= comment.body %>

83 |
84 |
85 |
86 | <% end %> 87 |
88 | <%= form_for @changeset, category_article_comment_path(@conn, :create, @category_id, @article.hash_id), fn f -> %> 89 | 90 |
91 | <%= label f, :username, class: "control-label" %> 92 | <%= text_input f, :name, class: "form-control", placeholder: "昵称", id: "username" %> 93 |
94 | 95 |
96 | <%= label f, :body, class: "control-label" %> 97 | <%= textarea f, :body, class: "form-control", placeholder: "至少输入5个字符", id: "comment-content", rows: "5", style: "resize: none; overflow: hidden; word-wrap: break-word; height: 174px;"%> 98 | 99 | 100 | 101 |
102 |
103 | 104 |
105 | 106 | <% end %> 107 | 108 |
109 |
110 |
111 |
112 | 113 | -------------------------------------------------------------------------------- /web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 博客 firebroo的个人网站 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | " rel="stylesheet"> 18 | 21 | 22 | 23 | 24 | 32 | 33 |
34 |
35 | 87 |
88 |
89 |
Stay Hungry.Stay Foolish.
90 |
91 |
92 | 93 |
94 |
95 |
96 | <%= if flash = get_flash(@conn, :info) do %> 97 |
98 | 99 | <%= flash %> 100 |
101 | <%= end %> 102 | <%= if flash = get_flash(@conn, :error) do %> 103 |
104 | 105 | <%= flash %> 106 |
107 | <%= end %> 108 |
109 |
110 |
111 | 112 | <%= render @view_module, @view_template, assigns %> 113 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /web/static/css/style.scss: -------------------------------------------------------------------------------- 1 | @mixin transform-action($style) { 2 | -webkit-transform: $style; 3 | -moz-transform: $style; 4 | -ms-transform: $style; 5 | -o-transform: $style; 6 | transform: $style; 7 | } 8 | 9 | @mixin image-animation($style) { 10 | -webkit-animation:widget-user-image $style; 11 | -moz-animation:widget-user-image $style; 12 | -ms-animation:widget-user-image $style; 13 | -o-animation:widget-user-image $style; 14 | animation:widget-user-image $style; 15 | } 16 | 17 | @keyframes widget-user-image { 18 | 0% { 19 | @include transform-action(rotateY(0deg)); 20 | } 21 | 100% { 22 | @include transform-action(rotateY(360deg)); 23 | } 24 | } 25 | 26 | .widget-user-image:hover { 27 | @include image-animation(2.5s linear infinite); 28 | } 29 | 30 | .widget-user-image { 31 | cursor: pointer; 32 | transform-style: preserve-3d; 33 | } 34 | 35 | .table { 36 | margin-top: 10px; 37 | .th-right { 38 | text-align:right; 39 | } 40 | form.link { 41 | display: inline-block; 42 | } 43 | } 44 | 45 | .table-title { 46 | text-align:center; 47 | } 48 | 49 | 50 | .box { 51 | background-color: #fff; 52 | border-radius: 3px; 53 | box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.1); 54 | border: 1px solid #e2e2e2; 55 | margin-bottom: 40px; 56 | .header { 57 | border-color: #ddd; 58 | padding:10px 0px; 59 | background-color: #f5f5f5; 60 | } 61 | hr { 62 | margin-top: 0px; 63 | } 64 | .elehead { 65 | margin-left: 5px; 66 | } 67 | .ff { 68 | float: right; 69 | margin-right: 5px; 70 | } 71 | .ele { 72 | margin: 10px; 73 | } 74 | } 75 | 76 | .form-signin-regist { 77 | max-width: 450px; 78 | padding: 19px 29px 29px; 79 | margin: 50px auto 20px; 80 | background-color: #fff; 81 | border: 1px solid #e5e5e5; 82 | -webkit-border-radius: 5px; 83 | -moz-border-radius: 5px; 84 | border-radius: 5px; 85 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05); 86 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.05); 87 | box-shadow: 0 1px 2px rgba(0,0,0,.05); 88 | input[type="text"], input[type="password"] { 89 | font-size: 16px; 90 | height: auto; 91 | margin-bottom: 20px; 92 | padding: 7px 9px; 93 | } 94 | h2 { 95 | margin-top: 0px; 96 | line-height: 40px; 97 | } 98 | span { 99 | vertical-align: middle; 100 | padding-top: 16px; 101 | float: right; 102 | font-size: 18px; 103 | line-height: 1.3333333; 104 | border-radius: 6px; 105 | } 106 | .form-signin-heading, .form-register-heading { 107 | margin-bottom: 10px; 108 | text-align: center; 109 | } 110 | } 111 | 112 | .form-register { 113 | @extend .form-signin-regist; 114 | } 115 | 116 | .form-signin { 117 | @extend .form-signin-regist; 118 | } 119 | 120 | .edit { 121 | border-radius: 3px; 122 | min-height: 270px; 123 | box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.1); 124 | border: 1px solid #e2e2e2; 125 | margin-bottom: 30px; 126 | .header { 127 | border-color: #ddd; 128 | padding:10px 15px; 129 | background-color: #f5f5f5; 130 | border-bottom: 1px solid transparent; 131 | border-top-right-radius: 3px; 132 | border-top-left-radius: 3px; 133 | } 134 | .body { 135 | margin: 25px 10px; 136 | } 137 | } 138 | 139 | .dropdown-toggle { 140 | color: #fff; 141 | &:hover { 142 | background-color: #52768e!important; 143 | } 144 | .avatar { 145 | width: 28px; 146 | height: 28px; 147 | border-radius: 50%; 148 | } 149 | } 150 | 151 | .nav .open>a, .nav .open>a:hover { 152 | color: #fff; 153 | background-color: #52768e!important; 154 | } 155 | 156 | 157 | 158 | 159 | .avatar-preview .media-object { 160 | display: inline-block; 161 | } 162 | 163 | @mixin avatar($width: 73px) { 164 | width: $width; 165 | height: $width; 166 | border-radius: 120px; 167 | } 168 | 169 | .avatar-96 { 170 | @include avatar(96px); 171 | } 172 | 173 | .avatar-48 { 174 | @include avatar(48px); 175 | } 176 | .avatar-32 { 177 | @include avatar(32px); 178 | } 179 | .avatar-16 { 180 | @include avatar(16px); 181 | } 182 | 183 | .hot-posts { 184 | a { 185 | overflow: hidden; 186 | white-space: nowrap; 187 | text-overflow: ellipsis; 188 | } 189 | } 190 | 191 | .navbar-self>li>a { 192 | margin:8px 10px; 193 | padding:5px 20px!important; 194 | border:1px solid #3f5b6e!important; 195 | border-radius:50px;background:rgba(82,118,142,.25)!important; 196 | color:#fff;font-size:1em; 197 | text-shadow:#666 1px 1px 2px; 198 | &:hover { 199 | color:#FF6E40; 200 | border:1px solid #FF6E40!important; 201 | background:#3f5b6e; 202 | } 203 | &:active { 204 | background:#3f5b6e; 205 | } 206 | } 207 | 208 | $text-shadow: 5px 5px 5px #3f5b6e; 209 | 210 | .navbar-header { 211 | a { 212 | color: #fff; 213 | font-size: 1.9em; 214 | text-decoration: none; 215 | text-shadow: $text-shadow; 216 | &:hover { 217 | color: #FF6E40; 218 | text-shadow: $text-shadow; 219 | } 220 | } 221 | .navbar-brand:hover { 222 | color: #FF6E40; 223 | } 224 | } 225 | 226 | header .description { 227 | font-size: 1.4em; 228 | text-shadow: $text-shadow; 229 | } 230 | 231 | .dropdown-menu { 232 | padding: 0px; 233 | margin-right: 10px; 234 | &>li>a:hover { 235 | background-color:#3f5b6e!important; 236 | } 237 | } 238 | 239 | table { 240 | width: 100%; /*表格宽度*/ 241 | max-width: 65em; /*表格最大宽度,避免表格过宽*/ 242 | border: 1px solid #dedede; /*表格外边框设置*/ 243 | margin: 15px auto; /*外边距*/ 244 | border-collapse: collapse; /*使用单一线条的边框*/ 245 | empty-cells: show; /*单元格无内容依旧绘制边框*/ 246 | table-layout:fixed; 247 | .over { 248 | overflow:hidden; 249 | white-space:nowrap; 250 | text-overflow:ellipsis; 251 | a { 252 | color: #555; 253 | &:visited, &:hover, &:link { 254 | text-decoration: none; 255 | } 256 | } 257 | } 258 | th { 259 | font-weight: bold; /*加粗*/ 260 | text-align: center !important; /*内容居中,加上 !important 避免被 Markdown 样式覆盖*/ 261 | background: rgba(158,188,226,0.2); /*背景色*/ 262 | } 263 | th, td { 264 | height: 35px; /*统一每一行的默认高度*/ 265 | border: 1px solid #dedede; /*内部边框样式*/ 266 | padding: 0 10px; /*内边距*/ 267 | } 268 | tr:hover { 269 | background: #efefef; 270 | } 271 | } 272 | 273 | .home { 274 | .preview-widget { 275 | float: left; 276 | padding: 10px; 277 | width: 20%; 278 | background: #fff; 279 | .item { 280 | height: 120px; 281 | border: 1px solid #ddd; 282 | text-align: center; 283 | padding: 10px; 284 | border-radius: 4px; 285 | .fa { 286 | font-size: 60px; 287 | } 288 | span { 289 | margin-top: 10px; 290 | display: block; 291 | } 292 | } 293 | .item1 .fa { 294 | color: #ffd52f; 295 | } 296 | .item2 .fa { 297 | color: #3bd54e; 298 | } 299 | .item3 .fa { 300 | color: #317dda; 301 | } 302 | .item4 .fa { 303 | color: #f86334; 304 | } 305 | .item5 .fa { 306 | color: #a94442; 307 | } 308 | } 309 | } 310 | --------------------------------------------------------------------------------