From c05803ff582daca72fca69620d78b97cb7c94155 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Thu, 1 May 2025 03:56:55 +0300 Subject: [PATCH] add contract md folder support --- actix_mvc_app/Cargo.lock | 35 ++++ actix_mvc_app/Cargo.toml | 1 + .../src/content/contract-003/1-purpose.md | 3 + .../contract-003/2-tokenization-process.md | 3 + .../content/contract-003/3-revenue-sharing.md | 3 + .../src/content/contract-003/4-governance.md | 3 + .../src/content/contract-003/appendix-a.md | 3 + .../src/content/contract-003/appendix-b.md | 3 + .../src/content/contract-003/appendix-c.md | 3 + .../src/content/contract-003/appendix-d.md | 3 + .../src/content/contract-003/cover.md | 3 + actix_mvc_app/src/controllers/contract.rs | 172 +++++++++++++++-- actix_mvc_app/src/models/contract.rs | 15 +- .../src/views/contracts/contract_detail.html | 181 +++++++++++------- .../contracts/macros/contract_macros.html | 10 + 15 files changed, 365 insertions(+), 76 deletions(-) create mode 100644 actix_mvc_app/src/content/contract-003/1-purpose.md create mode 100644 actix_mvc_app/src/content/contract-003/2-tokenization-process.md create mode 100644 actix_mvc_app/src/content/contract-003/3-revenue-sharing.md create mode 100644 actix_mvc_app/src/content/contract-003/4-governance.md create mode 100644 actix_mvc_app/src/content/contract-003/appendix-a.md create mode 100644 actix_mvc_app/src/content/contract-003/appendix-b.md create mode 100644 actix_mvc_app/src/content/contract-003/appendix-c.md create mode 100644 actix_mvc_app/src/content/contract-003/appendix-d.md create mode 100644 actix_mvc_app/src/content/contract-003/cover.md create mode 100644 actix_mvc_app/src/views/contracts/macros/contract_macros.html diff --git a/actix_mvc_app/Cargo.lock b/actix_mvc_app/Cargo.lock index d3cbc8b..b9a7026 100644 --- a/actix_mvc_app/Cargo.lock +++ b/actix_mvc_app/Cargo.lock @@ -259,6 +259,7 @@ dependencies = [ "lazy_static", "log", "num_cpus", + "pulldown-cmark", "redis", "serde", "serde_json", @@ -1075,6 +1076,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1931,6 +1941,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.40" @@ -2622,6 +2651,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/actix_mvc_app/Cargo.toml b/actix_mvc_app/Cargo.toml index 47f2a0b..3247ebc 100644 --- a/actix_mvc_app/Cargo.toml +++ b/actix_mvc_app/Cargo.toml @@ -23,3 +23,4 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] } lazy_static = "1.4.0" redis = { version = "0.23.0", features = ["tokio-comp"] } jsonwebtoken = "8.3.0" +pulldown-cmark = "0.13.0" diff --git a/actix_mvc_app/src/content/contract-003/1-purpose.md b/actix_mvc_app/src/content/contract-003/1-purpose.md new file mode 100644 index 0000000..d70f614 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/1-purpose.md @@ -0,0 +1,3 @@ +## 1. Purpose + +The purpose of this Agreement is to establish the terms and conditions for tokenizing real estate assets on the Zanzibar blockchain network. diff --git a/actix_mvc_app/src/content/contract-003/2-tokenization-process.md b/actix_mvc_app/src/content/contract-003/2-tokenization-process.md new file mode 100644 index 0000000..981567d --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/2-tokenization-process.md @@ -0,0 +1,3 @@ +## 2. Tokenization Process + +Tokenizer shall create digital tokens representing ownership interests in the properties listed in Appendix A according to the specifications in Appendix B. diff --git a/actix_mvc_app/src/content/contract-003/3-revenue-sharing.md b/actix_mvc_app/src/content/contract-003/3-revenue-sharing.md new file mode 100644 index 0000000..677c424 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/3-revenue-sharing.md @@ -0,0 +1,3 @@ +## 3. Revenue Sharing + +Revenue generated from the tokenized properties shall be distributed according to the formula set forth in Appendix C. diff --git a/actix_mvc_app/src/content/contract-003/4-governance.md b/actix_mvc_app/src/content/contract-003/4-governance.md new file mode 100644 index 0000000..5652c66 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/4-governance.md @@ -0,0 +1,3 @@ +## 4. Governance + +Decisions regarding the management of tokenized properties shall be made according to the governance framework outlined in Appendix D. diff --git a/actix_mvc_app/src/content/contract-003/appendix-a.md b/actix_mvc_app/src/content/contract-003/appendix-a.md new file mode 100644 index 0000000..4f4b59d --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/appendix-a.md @@ -0,0 +1,3 @@ +### Appendix A: Properties + +List of properties to be tokenized. diff --git a/actix_mvc_app/src/content/contract-003/appendix-b.md b/actix_mvc_app/src/content/contract-003/appendix-b.md new file mode 100644 index 0000000..fac8657 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/appendix-b.md @@ -0,0 +1,3 @@ +### Appendix B: Specifications + +Technical specifications for tokenization. diff --git a/actix_mvc_app/src/content/contract-003/appendix-c.md b/actix_mvc_app/src/content/contract-003/appendix-c.md new file mode 100644 index 0000000..d349025 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/appendix-c.md @@ -0,0 +1,3 @@ +### Appendix C: Revenue Formula + +Formula for revenue distribution. diff --git a/actix_mvc_app/src/content/contract-003/appendix-d.md b/actix_mvc_app/src/content/contract-003/appendix-d.md new file mode 100644 index 0000000..873cf08 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/appendix-d.md @@ -0,0 +1,3 @@ +### Appendix D: Governance Framework + +Governance framework for tokenized properties. diff --git a/actix_mvc_app/src/content/contract-003/cover.md b/actix_mvc_app/src/content/contract-003/cover.md new file mode 100644 index 0000000..a74eaa4 --- /dev/null +++ b/actix_mvc_app/src/content/contract-003/cover.md @@ -0,0 +1,3 @@ +# Digital Asset Tokenization Agreement + +This Digital Asset Tokenization Agreement (the "Agreement") is entered into between Zanzibar Property Consortium ("Tokenizer") and the property owners listed in Appendix A ("Owners"). diff --git a/actix_mvc_app/src/controllers/contract.rs b/actix_mvc_app/src/controllers/contract.rs index 0f125f0..ed860f6 100644 --- a/actix_mvc_app/src/controllers/contract.rs +++ b/actix_mvc_app/src/controllers/contract.rs @@ -3,8 +3,10 @@ use tera::{Context, Tera}; use chrono::{Utc, Duration}; use serde::Deserialize; use serde_json::json; +use actix_web::web::Query; +use std::collections::HashMap; -use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus}; +use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus, TocItem}; use crate::utils::render_template; #[derive(Debug, Deserialize)] @@ -105,7 +107,11 @@ impl ContractController { } // Display a specific contract - pub async fn detail(tmpl: web::Data, path: web::Path) -> Result { + pub async fn detail( + tmpl: web::Data, + path: web::Path, + query: Query> + ) -> Result { let contract_id = path.into_inner(); let mut context = Context::new(); @@ -126,9 +132,53 @@ impl ContractController { // Convert contract to JSON let contract_json = Self::contract_to_json(contract); - + // Add contract to context context.insert("contract", &contract_json); + + // If this contract uses multi-page markdown, load the selected section + println!("DEBUG: content_dir = {:?}, toc = {:?}", contract.content_dir, contract.toc); + if let (Some(content_dir), Some(toc)) = (&contract.content_dir, &contract.toc) { + use std::fs; + use pulldown_cmark::{Parser, Options, html}; + // Helper to flatten toc recursively + fn flatten_toc<'a>(items: &'a Vec, out: &mut Vec<&'a TocItem>) { + for item in items { + out.push(item); + if !item.children.is_empty() { + flatten_toc(&item.children, out); + } + } + } + let mut flat_toc = Vec::new(); + flatten_toc(&toc, &mut flat_toc); + let section_param = query.get("section"); + let selected_file = section_param + .and_then(|f| flat_toc.iter().find(|item| item.file == *f).map(|item| item.file.clone())) + .unwrap_or_else(|| flat_toc.get(0).map(|item| item.file.clone()).unwrap_or_default()); + context.insert("section", &selected_file); + let rel_path = format!("{}/{}", content_dir, selected_file); + let abs_path = match std::env::current_dir() { + Ok(dir) => dir.join(&rel_path), + Err(_) => std::path::PathBuf::from(&rel_path), + }; + println!("DEBUG: Attempting to read markdown file at absolute path: {:?}", abs_path); + match fs::read_to_string(&abs_path) { + Ok(md) => { + println!("DEBUG: Successfully read markdown file"); + let parser = Parser::new_ext(&md, Options::all()); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + context.insert("contract_section_content", &html_output); + }, + Err(e) => { + let error_msg = format!("Error: Could not read contract section markdown at '{:?}': {}", abs_path, e); + println!("{}", error_msg); + context.insert("contract_section_content_error", &error_msg); + } + } + context.insert("toc", &toc); + } // Count signed signers for the template let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count(); @@ -327,6 +377,8 @@ impl ContractController { // Mock contract 1 - Signed Service Agreement let mut contract1 = Contract { + content_dir: None, + toc: None, id: "contract-001".to_string(), title: "Digital Hub Service Agreement".to_string(), description: "Service agreement for cloud hosting and digital infrastructure services provided by the Zanzibar Digital Hub.".to_string(), @@ -381,6 +433,8 @@ impl ContractController { // Mock contract 2 - Pending Signatures let mut contract2 = Contract { + content_dir: None, + toc: None, id: "contract-002".to_string(), title: "Software Development Agreement".to_string(), description: "Agreement for custom software development services for the Zanzibar Digital Marketplace platform.".to_string(), @@ -450,7 +504,56 @@ impl ContractController { signers: Vec::new(), revisions: Vec::new(), current_version: 1, + content_dir: Some("src/content/contract-003".to_string()), + toc: Some(vec![ + TocItem { + title: "Cover".to_string(), + file: "cover.md".to_string(), + children: vec![], + }, + TocItem { + title: "1. Purpose".to_string(), + file: "1-purpose.md".to_string(), + children: vec![], + }, + TocItem { + title: "2. Tokenization Process".to_string(), + file: "2-tokenization-process.md".to_string(), + children: vec![], + }, + TocItem { + title: "3. Revenue Sharing".to_string(), + file: "3-revenue-sharing.md".to_string(), + children: vec![], + }, + TocItem { + title: "4. Governance".to_string(), + file: "4-governance.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix A: Properties".to_string(), + file: "appendix-a.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix B: Technical Specs".to_string(), + file: "appendix-b.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix C: Revenue Formula".to_string(), + file: "appendix-c.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix D: Governance Framework".to_string(), + file: "appendix-d.md".to_string(), + children: vec![], + }, + ]), }; + // Add potential signers to contract 3 (still in draft) contract3.signers.push(ContractSigner { @@ -471,17 +574,62 @@ impl ContractController { comments: None, }); - // Add revisions to contract 3 - contract3.revisions.push(ContractRevision { - version: 1, - content: "

Digital Asset Tokenization Agreement

This Digital Asset Tokenization Agreement (the \"Agreement\") is entered into between Zanzibar Property Consortium (\"Tokenizer\") and the property owners listed in Appendix A (\"Owners\").

1. Purpose

The purpose of this Agreement is to establish the terms and conditions for tokenizing real estate assets on the Zanzibar blockchain network.

2. Tokenization Process

Tokenizer shall create digital tokens representing ownership interests in the properties listed in Appendix A according to the specifications in Appendix B.

3. Revenue Sharing

Revenue generated from the tokenized properties shall be distributed according to the formula set forth in Appendix C.

4. Governance

Decisions regarding the management of tokenized properties shall be made according to the governance framework outlined in Appendix D.

".to_string(), - created_at: Utc::now() - Duration::days(3), - created_by: "Nala Okafor".to_string(), - comments: Some("Initial draft of the tokenization agreement.".to_string()), - }); + // Add ToC and content directory to contract 3 + contract3.content_dir = Some("src/content/contract-003".to_string()); + contract3.toc = Some(vec![ + TocItem { + title: "Digital Asset Tokenization Agreement".to_string(), + file: "cover.md".to_string(), + children: vec![ + TocItem { + title: "1. Purpose".to_string(), + file: "1-purpose.md".to_string(), + children: vec![], + }, + TocItem { + title: "2. Tokenization Process".to_string(), + file: "2-tokenization-process.md".to_string(), + children: vec![], + }, + TocItem { + title: "3. Revenue Sharing".to_string(), + file: "3-revenue-sharing.md".to_string(), + children: vec![], + }, + TocItem { + title: "4. Governance".to_string(), + file: "4-governance.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix A: Properties".to_string(), + file: "appendix-a.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix B: Specifications".to_string(), + file: "appendix-b.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix C: Revenue Formula".to_string(), + file: "appendix-c.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix D: Governance Framework".to_string(), + file: "appendix-d.md".to_string(), + children: vec![], + }, + ], + } + ]); + // No revision content for contract 3, content is in markdown files. // Mock contract 4 - Rejected let mut contract4 = Contract { + content_dir: None, + toc: None, id: "contract-004".to_string(), title: "Data Sharing Agreement".to_string(), description: "Agreement governing the sharing of anonymized data between Zanzibar Digital Hub and research institutions.".to_string(), @@ -528,6 +676,8 @@ impl ContractController { // Mock contract 5 - Active let mut contract5 = Contract { + content_dir: None, + toc: None, id: "contract-005".to_string(), title: "Digital Identity Verification Service Agreement".to_string(), description: "Agreement for providing digital identity verification services to businesses operating in the Zanzibar Autonomous Zone.".to_string(), diff --git a/actix_mvc_app/src/models/contract.rs b/actix_mvc_app/src/models/contract.rs index b4735da..ef936a5 100644 --- a/actix_mvc_app/src/models/contract.rs +++ b/actix_mvc_app/src/models/contract.rs @@ -136,6 +136,14 @@ impl ContractRevision { } } +/// Table of Contents item for multi-page contracts +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TocItem { + pub title: String, + pub file: String, + pub children: Vec, +} + /// Contract model #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Contract { @@ -153,6 +161,9 @@ pub struct Contract { pub revisions: Vec, pub current_version: u32, pub organization_id: Option, + // Multi-page markdown support + pub content_dir: Option, + pub toc: Option>, } impl Contract { @@ -171,8 +182,10 @@ impl Contract { expiration_date: None, signers: Vec::new(), revisions: Vec::new(), - current_version: 0, + current_version: 1, organization_id, + content_dir: None, + toc: None, } } diff --git a/actix_mvc_app/src/views/contracts/contract_detail.html b/actix_mvc_app/src/views/contracts/contract_detail.html index 0568c91..376124e 100644 --- a/actix_mvc_app/src/views/contracts/contract_detail.html +++ b/actix_mvc_app/src/views/contracts/contract_detail.html @@ -1,3 +1,5 @@ +{% import "contracts/macros/contract_macros.html" as contract_macros %} + {% extends "base.html" %} {% block title %}Contract Details{% endblock %} @@ -41,6 +43,11 @@ Activity +
@@ -53,83 +60,52 @@
Contract Document
{% if contract.status == 'Signed' %} - SIGNED + SIGNED {% elif contract.status == 'Active' %} - ACTIVE + ACTIVE {% elif contract.status == 'PendingSignatures' %} - PENDING + PENDING {% elif contract.status == 'Draft' %} - DRAFT + DRAFT {% endif %}
- {% if contract.revisions|length > 0 %} + {% if contract_section_content_error is defined %} +
{{ contract_section_content_error }}
+ {% endif %} + {% if contract_section_content is defined %} +
+
+
+ {% set section_param = section | default(value=toc[0].file) %} + {{ contract_macros::render_toc(items=toc, section_param=section_param) }} +
+
+
+
+ {{ contract_section_content | safe }} +
+
+
+ {% elif contract.revisions|length > 0 %} {% set latest_revision = contract.latest_revision %}
{{ latest_revision.content|safe }}
{% else %} -
-

No content has been added to this contract yet.

+
+

+ {% if contract_section_content_error is defined %} + {{ contract_section_content_error }} + {% else %} + No content or markdown sections could be loaded for this contract. Please check the contract's content directory and Table of Contents configuration. + {% endif %} +

{% endif %}
- - -
-
-
Signatures
-
-
-
- {% for signer in contract.signers %} -
-
-
-
{{ signer.name }}
- - {{ signer.status }} - -
-
-

{{ signer.email }}

- - {% if signer.status == 'Signed' %} -
- Signature -
Signed on {{ signer.signed_at }}
-
- {% elif signer.status == 'Rejected' %} -
- Rejected on {{ signer.signed_at }} -
- {% else %} -
-

Waiting for signature...

- {% if not user_has_signed %} - - {% endif %} -
- {% endif %} - - {% if signer.comments %} -
-

Comments:

-

{{ signer.comments }}

-
- {% endif %} -
-
-
- {% endfor %} -
-
-
-
@@ -168,7 +144,6 @@
-
Signers Status
@@ -195,7 +170,6 @@
-
Contract Info
@@ -223,7 +197,86 @@
- + + +
+
+
+
+
+
Signatures
+
+
+
+ + + + + + + + + + + + + {% for signer in contract.signers %} + + + + + + + + + {% endfor %} + +
NameEmailStatusSigned AtCommentsActions
{{ signer.name }}{{ signer.email }} + + {{ signer.status }} + + + {% if signer.status == 'Signed' or signer.status == 'Rejected' %} + {{ signer.signed_at }} + {% else %} + -- + {% endif %} + + {% if signer.comments %} + {{ signer.comments }} + {% else %} + -- + {% endif %} + + {% if signer.status == 'Signed' %} + + View Signed Document + + {% elif signer.status == 'Rejected' %} + + + {% else %} + {% if current_user is defined and not user_has_signed and signer.email == current_user.email %} + + {% endif %} + + {% endif %} +
+
+
+
+
+
+
+
diff --git a/actix_mvc_app/src/views/contracts/macros/contract_macros.html b/actix_mvc_app/src/views/contracts/macros/contract_macros.html new file mode 100644 index 0000000..e66fbdc --- /dev/null +++ b/actix_mvc_app/src/views/contracts/macros/contract_macros.html @@ -0,0 +1,10 @@ +{% macro render_toc(items, section_param) %} + {% for item in items %} + {{ item.title }} + {% if item.children and item.children | length > 0 %} +
+ {{ self::render_toc(items=item.children, section_param=section_param) }} +
+ {% endif %} + {% endfor %} +{% endmacro %}