添加类别标签

Category.swift

给Categor模型添加一个扩展方法,用来给缩略语添加类别标签

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension Category {
    ...

    static func addCategory(_ name: String,
                            to acronym: Acronym,
                            on req: Request
        ) throws -> Future<Void> {
        return Category.query(on: req).filter(\.name == name).first()
            .flatMap(to: Void.self) { foundCategory in
                if let existingCategory = foundCategory {
                    return acronym.categories.attach(existingCategory, on: req)
                    .transform(to: ())
                }
                else {
                    let category = Category(name: name)
                    return category.save(on: req).flatMap(to: Void.self) { savedCategory in
                        return acronym.categories.attach(savedCategory, on: req).transform(to: ())

                    }
                }
        }
    }
}

WebsiteController.swift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
struct CreateAcronymData: Content {
    let userID: User.ID
    let short: String
    let long: String
    let categories: [String]?
}
...
router.post(CreateAcronymData.self, at: "acronyms", "create", use: createAcronymPostHandler)
...
func createAcronymPostHandler(_ req: Request, data: CreateAcronymData) throws -> Future<Response> {

        let acronym = Acronym(short: data.short, long: data.long, userID: data.userID)


        return acronym.save(on: req).flatMap(to: Response.self) { acronym in
            guard let id = acronym.id else {
                throw Abort(HTTPStatus.internalServerError)
            }
            var categorySaves: [Future<Void>] = []
            for category in data.categories ?? [] {
                try categorySaves.append(Category.addCategory(category, to: acronym, on: req))
            }
            let redirect = req.redirect(to: "/acronyms/\(id)")
            return categorySaves.flatten(on: req).transform(to: redirect)
        }
    }
...

createAcronym.leaf

categories[]可发送经url编码后的categories数组数据, multiple允许多选

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    ...
        <div class="form-group">
            <label for="categories">Categories</label>
            <select name="categories[]" class="form-control" id="categories" placeholder="Categories" multiple="multiple">
            </select>
        </div>
        <button type="submit" class = "btn btn-primary">
            #if(editing) { Update } else { Submit }
        </button>
    </form>
}
...

empty-add-categories

因为我们目前的缩略语还没有相关类别,所以显示是空的,也不好看,所以我们使用一个叫作Select2的javascript库来美化一下。因为jQuery的slim版本不包含select2需要用到的函数,所以换了一个版本的jQuery。同时本地写了一个js,也包含在模板文件中了。

base.leaf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
<head>
    ...
    #if(title=="Create An Acronym") {
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.css" />
    }
    #if(title=="Edit Acronym"){
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.css" />
    }
<title>#(title) | Acronyms</title>
</head>
...
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

#if(title=="Create An Acronym") {
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/js/select2.min.js"></script>
    <script src="/scripts/createAcronym.js"></script>
}
#if(title == "Edit Acronym"){
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/js/select2.min.js"></script>
    <script src="/scripts/createAcronym.js"></script>
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</body>

Public/scripts/createAcronym.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$.ajax({
       url: "/api/categories/",
       type: "GET",
       contentType: "application/json; charset=utf-8"
       })
.then(function (response) {
      var dataToReturn = [];

      for(var i = 0; i < response.length; i++) {
      var tagToTransform = response[i];
      var newTag = {
      id: tagToTransform["name"],
      text: tagToTransform["name"]
      };

      dataToReturn.push(newTag);
      }

      $("#categories").select2({
                               placeholder: "Select Categories for the Acronym",
                               tags: true,
                               tokenSeparator: [','],
                               data: dataToReturn
                               });
      });

select2 category tags

acronym.leaf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#set("content"){
...
    #if(count(categories) > 0) {
        <h3>Categories</h3>
        <ul>
            #for(category in categories) {
            <li>
                <a href="/category/#(category.id)">
                    #(category.name)
                </a>
            </li>
            }
        </ul>
    }
    <form method="post" action="/acronyms/#(acronym.id)/delete">
        <a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit" role="button">Edit</a>&nbsp;
        <input class="btn btn-danger" type="submit" value="Delete" />
    </form>
}

#embed("base")

WebsiteController.swift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
struct AcronymContext: Encodable {
    let title: String
    let acronym: Acronym
    let user: User
    let categories: Future<[Category]>
}
...
func acronymHandler(_ req: Request) throws -> Future<View> {
    return try req.parameters.next(Acronym.self)
        .flatMap(to: View.self) { acronym in
            return acronym.user.get(on: req)
                .flatMap(to: View.self) { user in
                    let categories = try acronym.categories.query(on: req).all()
                    let context = AcronymContext(
                        title: acronym.short,
                        acronym: acronym,
                        user: user,
                        categories: categories)
                    return try req.view().render("acronym", context)
            }
    }
}
...

acronym categories

目前只是实现了选择类别和显示类别的功能,但是你在编辑缩略语时会发现,选择的类别没有被保存下来。

WebsiteController.swift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import Fluent
...
struct EditAcronymContext: Encodable {
    let title = "Edit Acronym"
    let acronym: Acronym
    let users: Future<[User]>
    let editing = true
    let categories: Future<[Category]>
}
...
    func editAcronymHandler(_ req: Request) throws -> Future<View> {
        return try req.parameters.next(Acronym.self)
            .flatMap(to: View.self) { acronym in
                let users = User.query(on: req).all()
                let categories = try acronym.categories.query(on: req).all()
                let context = EditAcronymContext(acronym: acronym,
                                                 users: users,
                                                 categories: categories)
                return try req.view().render("createAcronym", context)
        }
    }
    ...
    func editAcronymPostHandler(_ req: Request) throws -> Future<Response> {
        return try flatMap(to: Response.self,
                           req.parameters.next(Acronym.self),
                           req.content.decode(CreateAcronymData.self)
        ) { acronym, data in
            acronym.short = data.short
            acronym.long = data.long
            acronym.userID = data.userID

            return acronym.save(on: req).flatMap(to: Response.self) { savedAcronym in
                guard let id = savedAcronym.id else {
                    throw Abort(HTTPResponseStatus.internalServerError)
                }

                return try acronym.categories.query(on: req).all()
                    .flatMap(to: Response.self) { existingCategories in
                        let existingStringArry = existingCategories.map { $0.name }

                        let existingSet = Set<String>(existingStringArry)
                        let newSet = Set<String>(data.categories ?? [])

                        let categoriesToAdd = newSet.subtracting(existingSet)
                        let categoriesToRemove = existingSet.subtracting(newSet)

                        var categoryResults: [Future<Void>] = []

                        for newCategory in categoriesToAdd {
                            categoryResults.append(try Category.addCategory(newCategory, to: acronym, on: req))
                        }

                        for categoryNameToRemove in categoriesToRemove {
                            let categoryToRemove = existingCategories.first {
                                $0.name == categoryNameToRemove
                            }
                            if let category = categoryToRemove {
                                categoryResults.append(acronym.categories.detach(category, on: req))
                            }
                        }
                        return categoryResults.flatten(on: req)
                        .transform(to: req.redirect(to: "/acronyms/\(id)"))
                }
            }
        }
    }    

acronym edit categories