notion 1.0.5 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 529d9a75b50731156f263184b27628a2ee6b0b0cd798e3f3b44e5ea3f7106c60
4
- data.tar.gz: eb5b69ebe9b9d34215d6da17e8c2999761e9242b2618afaaa6583ee09691c65d
3
+ metadata.gz: 3114ccf59f80a17322c559fce61050e7e7f1908a279e8786c4a8fe87e6fa79d8
4
+ data.tar.gz: a7c6fea5eb18a4d962117a4a5120bc357e0bf038640ba39db821d5bc3de230e6
5
5
  SHA512:
6
- metadata.gz: '09ec6f516f22fac9f2b9c7c2efc3df164f83a745414cb18f092a6df939625435a77883712b86e100d7062536b189ddd8115145a52528cd5cc81767029f3bd250'
7
- data.tar.gz: b643a46d9682d2ebb612c0d0c37454344f0f9abb580c1cf258887bc7a242afdc22af155c78b87434a00c38a048b206351ad453477d5c914393b1ff4ac68aeb97
6
+ metadata.gz: '06202342711853b35c0d694b4de7c89aec269f226d5ceda2c6b4e086f61f6e8f3e7381ca1d0389d85c2b3814d849dd980d9d6b3492a9d2a3d9c8bf9fbad71939'
7
+ data.tar.gz: 7561a58fff0a26b4190a79c713a4f89c3f85c1c5cca4bca21e2c9013ea3fa981f32e8779e9b771b243fe41a65c5b717f9a09e675e9c9590c3a09cf1f344ca02b
data/README.md CHANGED
@@ -310,8 +310,12 @@ From here, you can instantiate the Notion Client with the following code:
310
310
  )
311
311
  ```
312
312
  ### Retrieve a full-page Collection View
313
- Currently, either a "normal" Page URL or the Page Block ID is accepted to the `get_page` method. Therefore, if you pass the full URL to the CV Table, it will raise an error:
314
- ```text
315
- the URL or ID passed to the get_page method must be that of a Page Block.
313
+ A full-page collection view must have a URL that follows the below pattern:
314
+ https://www.notion.so/danmurphy/[page-id]?v=[view-id]
315
+ Then, it can be retrieved with the following code:
316
+ ```ruby
317
+ >>> @client = NotionAPI::Client.new(
318
+ "<insert_v2_token_here>"
319
+ )
320
+ >>> @client.get_page(https://www.notion.so/danmurphy/[page-id]?v=[view-id])
316
321
  ```
317
- To avoid this, you must pass only the ID of the full-page collection-view to the `get_page` method. This is next up on the features list, so passing the full URL will be supported soon:smile:
@@ -235,17 +235,26 @@ module NotionAPI
235
235
  # ! parse and clean the URL or ID object provided.
236
236
  # ! url_or_id -> the block ID or URL : ``str``
237
237
  http_or_https = url_or_id.match(/^(http|https)/) # true if http or https in url_or_id...
238
+ collection_view_match = url_or_id.match(/(\?v=)/)
239
+
238
240
  if (url_or_id.length == 36) && ((url_or_id.split('-').length == 5) && !http_or_https)
239
241
  # passes if url_or_id is perfectly formatted already...
240
242
  url_or_id
241
- elsif (http_or_https && (url_or_id.split('-').last.length == 32)) || (!http_or_https && (url_or_id.length == 32))
243
+ elsif (http_or_https && (url_or_id.split('-').last.length == 32)) || (!http_or_https && (url_or_id.length == 32)) || (collection_view_match)
242
244
  # passes if either:
243
245
  # 1. a URL is passed as url_or_id and the ID at the end is 32 characters long or
244
246
  # 2. a URL is not passed and the ID length is 32 [aka unformatted]
245
247
  pattern = [8, 13, 18, 23]
246
- id = url_or_id.split('-').last
247
- pattern.each { |index| id.insert(index, '-') }
248
- id
248
+ if collection_view_match
249
+ id_without_view = url_or_id.split('?')[0]
250
+ clean_id = id_without_view.split('/').last
251
+ pattern.each { |index| clean_id.insert(index, '-') }
252
+ clean_id
253
+ else
254
+ id = url_or_id.split('-').last
255
+ pattern.each { |index| id.insert(index, '-') }
256
+ id
257
+ end
249
258
  else
250
259
  raise ArgumentError, 'Expected a Notion page URL or a page ID. Please consult the documentation for further information.'
251
260
  end
@@ -33,7 +33,9 @@ module NotionAPI
33
33
  transaction_id = extract_id(SecureRandom.hex(16))
34
34
  space_id = extract_id(SecureRandom.hex(16))
35
35
  new_block_id = extract_id(SecureRandom.hex(16))
36
- schema = extract_collection_schema(@collection_id, @view_id)
36
+ collection_data = extract_collection_data(collection_id, view_id)
37
+ last_row_id = collection_data["collection_view"][@view_id]["value"]["page_sort"][-1]
38
+ schema = collection_data["collection"][collection_id]["value"]["schema"]
37
39
  keys = schema.keys
38
40
  col_map = {}
39
41
  keys.map { |key| col_map[schema[key]["name"]] = key }
@@ -47,17 +49,28 @@ module NotionAPI
47
49
  instantiate_row = Utils::CollectionViewComponents.add_new_row(new_block_id)
48
50
  set_block_alive = Utils::CollectionViewComponents.set_collection_blocks_alive(new_block_id, @collection_id)
49
51
  new_block_edited_time = Utils::BlockComponents.last_edited_time(new_block_id)
50
- parent_edited_time = Utils::BlockComponents.last_edited_time(@parent_id)
52
+ page_sort = Utils::BlockComponents.row_location_add(last_row_id, new_block_id, @view_id)
51
53
 
52
54
  operations = [
53
55
  instantiate_row,
54
56
  set_block_alive,
55
57
  new_block_edited_time,
56
- parent_edited_time,
58
+ page_sort,
57
59
  ]
58
60
 
59
61
  data.keys.each_with_index do |col_name, j|
60
- child_component = Utils::CollectionViewComponents.insert_data(new_block_id, j.zero? ? "title" : col_map[col_name], data[col_name], j.zero? ? schema["title"]["type"] : schema[col_map[col_name]]["type"])
62
+ unless col_map.keys.include?(col_name.to_s); raise ArgumentError, "Column '#{col_name.to_s}' does not exist." end
63
+ if %q[select multi_select].include?(schema[col_map[col_name.to_s]]["type"])
64
+ options = schema[col_map[col_name.to_s]]["options"].nil? ? [] : schema[col_map[col_name.to_s]]["options"].map { |option| option["value"] }
65
+ multi_select_multi_options = data[col_name].split(",")
66
+ multi_select_multi_options.each do |option|
67
+ if !options.include?(option.strip)
68
+ create_new_option = Utils::CollectionViewComponents.add_new_option(col_map[col_name.to_s], option.strip, @collection_id)
69
+ operations.push(create_new_option)
70
+ end
71
+ end
72
+ end
73
+ child_component = Utils::CollectionViewComponents.insert_data(new_block_id, col_map[col_name.to_s], data[col_name], schema[col_map[col_name.to_s]]["type"])
61
74
  operations.push(child_component)
62
75
  end
63
76
 
@@ -73,7 +86,18 @@ module NotionAPI
73
86
  unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}.
74
87
  Please try again, and if issues persist open an issue in GitHub."; end
75
88
 
76
- NotionAPI::CollectionViewRow.new(new_block_id, @parent_id, @collection_id, @view_id)
89
+ collection_row = NotionAPI::CollectionViewRow.new(new_block_id, @parent_id, @collection_id, @view_id)
90
+
91
+ properties = {}
92
+ data.keys.each do |col|
93
+ properties[col_map[col.to_s]] = [[data[col]]]
94
+ end
95
+
96
+ collection_data["block"][collection_row.id] = { "role" => "editor", "value" => { "id" => collection_row.id, "version" => 12, "type" => "page", "properties" => properties, "created_time" => 1607253360000, "last_edited_time" => 1607253360000, "parent_id" => "dde513c6-2428-4a5d-a830-7a67fdbf6b48", "parent_table" => "collection", "alive" => true, "created_by_table" => "notion_user", "created_by_id" => "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", "last_edited_by_table" => "notion_user", "last_edited_by_id" => "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", "shard_id" => 955090, "space_id" => "f687f7de-7f4c-4a86-b109-941a8dae92d2" } }
97
+ row_data = collection_data["block"][collection_row.id]
98
+ create_singleton_methods_and_instance_variables(collection_row, row_data)
99
+
100
+ collection_row
77
101
  end
78
102
 
79
103
  def add_property(name, type)
@@ -204,6 +228,7 @@ module NotionAPI
204
228
  end
205
229
  clean_row_instances
206
230
  end
231
+
207
232
  def create_singleton_methods_and_instance_variables(row, row_data)
208
233
  # ! creates singleton methods for each property in a CollectionView.
209
234
  # ! row -> the block ID of the 'row' to retrieve: ``str``
@@ -213,22 +238,25 @@ module NotionAPI
213
238
  column_mappings = schema.keys
214
239
  column_hash = {}
215
240
  column_names = column_mappings.map { |mapping| column_hash[mapping] = schema[mapping]["name"].downcase }
216
-
241
+
217
242
  column_hash.keys.each_with_index do |column, i|
218
243
  # loop over the column names...
219
244
  # set instance variables for each column, allowing the dev to 'read' the column value
220
- cleaned_column = column_hash[column].split(" ").join("_").downcase.to_sym
245
+ cleaned_column = clean_property_names(column_hash, column)
221
246
 
222
247
  # p row_data["value"]["properties"][column_mappings[i]], !(row_data["value"]["properties"][column] or row_data["value"]["properties"][column_mappings[i]])
223
248
  if row_data["value"]["properties"].nil? or row_data["value"]["properties"][column].nil?
224
249
  value = ""
225
250
  else
226
- value = row_data["value"]["properties"][column][0][0]
251
+ value = row_data["value"]["properties"][column][0][0]
252
+ if ["‣"].include?(value.to_s)
253
+ value = row_data["value"]["properties"][column][0][1].flatten[-1]
254
+ end
227
255
  end
228
256
 
229
257
  row.instance_variable_set("@#{cleaned_column}", value)
230
258
  CollectionViewRow.class_eval { attr_reader cleaned_column }
231
- # then, define singleton methods for each column that are used to update the table cell
259
+ # then, define singleton methods for each column that are used to update the table cell
232
260
  row.define_singleton_method("#{cleaned_column}=") do |new_value|
233
261
  # neat way to get the name of the currently invoked method...
234
262
  parsed_method = __method__.to_s[0...-1].split("_").join(" ")
@@ -245,12 +273,23 @@ module NotionAPI
245
273
  space_id: space_id,
246
274
  }
247
275
 
248
- update_property_value = Utils::CollectionViewComponents.update_property_value(@id, column_hash.key(parsed_method), new_value)
276
+ update_property_value_hash = Utils::CollectionViewComponents.update_property_value(@id, column_hash.key(parsed_method), new_value)
249
277
 
250
278
  operations = [
251
- update_property_value,
279
+ update_property_value_hash,
252
280
  ]
253
281
 
282
+ if %q[select multi_select].include?(schema[column_hash.key(parsed_method)]["type"])
283
+ options = schema[column_hash.key(parsed_method)]["options"].nil? ? [] : schema[column_hash.key(parsed_method)]["options"].map { |option| option["value"] }
284
+ multi_select_multi_options = new_value.split(",")
285
+ multi_select_multi_options.each do |option|
286
+ if !options.include?(option.strip)
287
+ create_new_option = Utils::CollectionViewComponents.add_new_option(column_hash.key(parsed_method), option.strip, @collection_id)
288
+ operations.push(create_new_option)
289
+ end
290
+ end
291
+ end
292
+
254
293
  request_url = URLS[:UPDATE_BLOCK]
255
294
  request_body = build_payload(operations, request_ids)
256
295
  response = HTTParty.post(
@@ -260,14 +299,23 @@ module NotionAPI
260
299
  headers: headers,
261
300
  )
262
301
  unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}.
263
- Please try again, and if issues persist open an issue in GitHub.";end
264
-
302
+ Please try again, and if issues persist open an issue in GitHub."; end
303
+
265
304
  # set the instance variable to the updated value!
266
305
  _ = row.instance_variable_set("@#{__method__.to_s[0...-1]}", new_value)
267
306
  row
268
307
  end
269
308
  end
270
309
  end
310
+
311
+ def clean_property_names(prop_hash, prop_notion_name)
312
+ # ! standardize property names by splitting the words in the property name into an array, removing non-alphanumeric
313
+ # ! characters, downcasing, and then re-joining the array with underscores.
314
+ # ! prop_hash -> hash of property notion names and property textual names: ``str``
315
+ # ! prop_notion_name -> the four-character long name of the notion property: ``str``
316
+
317
+ prop_hash[prop_notion_name].split(" ").map { |word| word.gsub(/[^a-z0-9]/i, "").downcase }.join("_").to_sym
318
+ end
271
319
  end
272
320
 
273
321
  # class that represents each row in a CollectionView
@@ -200,14 +200,14 @@ module Utils
200
200
 
201
201
  args = if command == "listAfter"
202
202
  {
203
- after: target || block_id,
204
- id: new_block_id || block_id,
205
- }
203
+ after: target || block_id,
204
+ id: new_block_id || block_id,
205
+ }
206
206
  else
207
207
  {
208
- before: target || block_id,
209
- id: new_block_id || block_id,
210
- }
208
+ before: target || block_id,
209
+ id: new_block_id || block_id,
210
+ }
211
211
  end
212
212
 
213
213
  {
@@ -219,6 +219,21 @@ module Utils
219
219
  }
220
220
  end
221
221
 
222
+ def self.row_location_add(last_row_id, block_id, view_id)
223
+ {
224
+ "table": "collection_view",
225
+ "id": view_id,
226
+ "path": [
227
+ "page_sort"
228
+ ],
229
+ "command": "listAfter",
230
+ "args": {
231
+ "after": last_row_id,
232
+ "id": block_id
233
+ }
234
+ }
235
+ end
236
+
222
237
  def self.block_location_remove(block_parent_id, block_id)
223
238
  # ! removes a notion block
224
239
  # ! block_parent_id -> the parent ID of the block to remove : ``str``
@@ -276,9 +291,9 @@ module Utils
276
291
  def self.add_emoji_icon(block_id, icon)
277
292
  {
278
293
  id: block_id,
279
- table:"block",
280
- path:["format","page_icon"],
281
- command:"set","args": icon
294
+ table: "block",
295
+ path: ["format", "page_icon"],
296
+ command: "set", "args": icon,
282
297
  }
283
298
  end
284
299
  end
@@ -383,14 +398,14 @@ module Utils
383
398
  # ! new_block_id -> id of the new block
384
399
  # ! data -> json data to insert into table.
385
400
  col_names = data[0].keys
386
- data_mappings = {Integer => "number", String => "text", Array => "text", Float => "number", Date => "date"}
401
+ data_mappings = { Integer => "number", String => "text", Array => "text", Float => "number", Date => "date" }
387
402
  exceptions = [ArgumentError, TypeError]
388
403
  data_types = col_names.map do |name|
389
404
  # TODO: this is a little hacky... should probably think about a better way or add a requirement for user input to match a certain criteria.
390
- begin
405
+ begin
391
406
  DateTime.parse(data[0][name]) ? data_mappings[Date] : nil
392
407
  rescue *exceptions
393
- data_mappings[data[0][name].class]
408
+ data_mappings[data[0][name].class]
394
409
  end
395
410
  end
396
411
 
@@ -403,18 +418,18 @@ module Utils
403
418
  end
404
419
  end
405
420
  return {
406
- id: collection_id,
407
- table: "collection",
408
- path: [],
409
- command: "update",
410
- args: {
411
- id: collection_id,
412
- schema: schema_conf,
413
- parent_id: new_block_id,
414
- parent_table: "block",
415
- alive: true,
416
- },
417
- }, data_types
421
+ id: collection_id,
422
+ table: "collection",
423
+ path: [],
424
+ command: "update",
425
+ args: {
426
+ id: collection_id,
427
+ schema: schema_conf,
428
+ parent_id: new_block_id,
429
+ parent_table: "block",
430
+ alive: true,
431
+ },
432
+ }, data_types
418
433
  end
419
434
 
420
435
  def self.set_collection_title(collection_title, collection_id)
@@ -440,6 +455,11 @@ module Utils
440
455
  # ! column -> the name of the column to insert data into.
441
456
  # ! value -> the value to insert into the column.
442
457
  # ! mapping -> the column data type.
458
+ simple_mappings = ["title", "text", "phone_number", "email", "url", "number", "checkbox", "select", "multi_select"]
459
+ datetime_mappings = ["date"]
460
+ media_mappings = ["file"]
461
+ person_mappings = ["person"]
462
+
443
463
  table = "block"
444
464
  path = [
445
465
  "properties",
@@ -447,12 +467,50 @@ module Utils
447
467
  ]
448
468
  command = "set"
449
469
 
470
+ if simple_mappings.include?(mapping)
471
+ args = [[value]]
472
+ elsif media_mappings.include?(mapping)
473
+ args = [[value, [["a", value]]]]
474
+ elsif datetime_mappings.include?(mapping)
475
+ args = [["‣", [["d", { "type": "date", "start_date": value }]]]]
476
+ elsif person_mappings.include?(mapping)
477
+ args = [["‣",
478
+ [["u", value]]
479
+ ]]
480
+ else
481
+ raise SchemaTypeError, "Invalid property type: #{mapping}"
482
+ end
483
+
450
484
  {
451
- id: block_id,
452
485
  table: table,
486
+ id: block_id,
487
+ command: command,
453
488
  path: path,
489
+ args: args,
490
+ }
491
+ end
492
+
493
+ def self.add_new_option(column, value, collection_id)
494
+ table = "collection"
495
+ path = ["schema", column, "options"]
496
+ command = "keyedObjectListAfter"
497
+ colors = ["default", "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"]
498
+ random_color = colors[rand(0...colors.length)]
499
+
500
+ args = {
501
+ "value": {
502
+ "id": SecureRandom.hex(16),
503
+ "value": value,
504
+ "color": random_color
505
+ }
506
+ }
507
+
508
+ {
509
+ table: table,
510
+ id: collection_id,
454
511
  command: command,
455
- args: mapping == "date" ? [["‣",[["d",{"type": "date","start_date": value}]]]] : [[value]],
512
+ path: path,
513
+ args: args,
456
514
  }
457
515
  end
458
516
 
@@ -505,45 +563,45 @@ module Utils
505
563
  args["format"] = {
506
564
  "table_properties" => [
507
565
  {
508
- "property" => "title",
509
- "visible" => true,
510
- "width" => 280,
511
- },
566
+ "property" => "title",
567
+ "visible" => true,
568
+ "width" => 280,
569
+ },
512
570
  {
513
- "property" => "aliases",
514
- "visible" => true,
515
- "width" => 200,
516
- },
571
+ "property" => "aliases",
572
+ "visible" => true,
573
+ "width" => 200,
574
+ },
517
575
  {
518
- "property" => "category",
519
- "visible" => true,
520
- "width" => 200,
521
- },
576
+ "property" => "category",
577
+ "visible" => true,
578
+ "width" => 200,
579
+ },
522
580
  {
523
- "property" => "description",
524
- "visible" => true,
525
- "width" => 200,
526
- },
581
+ "property" => "description",
582
+ "visible" => true,
583
+ "width" => 200,
584
+ },
527
585
  {
528
- "property" => "ios_version",
529
- "visible" => true,
530
- "width" => 200,
531
- },
586
+ "property" => "ios_version",
587
+ "visible" => true,
588
+ "width" => 200,
589
+ },
590
+ {
591
+ "property" => "tags",
592
+ "visible" => true,
593
+ "width" => 200,
594
+ },
532
595
  {
533
- "property" => "tags",
534
- "visible" => true,
535
- "width" => 200,
536
- },
537
- {
538
596
  "property" => "phone",
539
597
  "visible" => true,
540
598
  "width" => 200,
541
599
  },
542
600
  {
543
- "property" => "unicode_version",
544
- "visible" => true,
545
- "width" => 200,
546
- }
601
+ "property" => "unicode_version",
602
+ "visible" => true,
603
+ "width" => 200,
604
+ },
547
605
  ],
548
606
  }
549
607
  {
@@ -563,11 +621,11 @@ module Utils
563
621
  table = "block"
564
622
  path = [
565
623
  "properties",
566
- column_name
624
+ column_name,
567
625
  ]
568
626
  command = "set"
569
627
  args = [[
570
- new_value
628
+ new_value,
571
629
  ]]
572
630
 
573
631
  {
@@ -575,11 +633,18 @@ module Utils
575
633
  table: table,
576
634
  path: path,
577
635
  command: command,
578
- args: args
636
+ args: args,
579
637
  }
580
638
  end
581
639
  end
582
640
 
641
+ class SchemaTypeError < StandardError
642
+ def initialize(msg="Custom exception that is raised when an invalid property type is passed as a mapping.", exception_type="schema_type")
643
+ @exception_type = exception_type
644
+ super(msg)
645
+ end
646
+ end
647
+
583
648
  def build_payload(operations, request_ids)
584
649
  # ! properly formats the payload for Notions backend.
585
650
  # ! operations -> an array of hashes that define the operations to perform : ``Array[Hash]``
@@ -600,4 +665,4 @@ module Utils
600
665
  }
601
666
  payload
602
667
  end
603
- end
668
+ end
@@ -1,3 +1,3 @@
1
1
  module NotionAPI
2
- VERSION = '1.0.5'
2
+ VERSION = '1.1.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notion
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Murphy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-04 00:00:00.000000000 Z
11
+ date: 2020-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty