{"id":867,"date":"2025-05-23T09:42:12","date_gmt":"2025-05-23T09:42:12","guid":{"rendered":"https:\/\/eipsoftware.com\/musings\/?p=867"},"modified":"2025-05-23T10:35:07","modified_gmt":"2025-05-23T10:35:07","slug":"microsoft-word-merge-file-app","status":"publish","type":"post","link":"https:\/\/eipsoftware.com\/musings\/microsoft-word-merge-file-app\/","title":{"rendered":"Microsoft Word &#8211; Merge File App"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This app will use a web interface to facilitate merging content into a Microsoft Word file.  It is the equivalent of the mail merge features with some notable improvements. The repo is available on github at <a href=\"https:\/\/github.com\/mmooney512\/musings\/tree\/main\/python\/word_file_merge_app\" target=\"_blank\" rel=\"noreferrer noopener\">github &#8211; word_file_merge_app<\/a><\/p>\n\n\n\n<!--more-->\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Advantages<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Web Interface<\/li>\n\n\n\n<li>Group your replacements by category<\/li>\n\n\n\n<li>Unlimited templates<\/li>\n\n\n\n<li>Unlimited placeholder and replacements<\/li>\n\n\n\n<li>Replacements can be\n<ul class=\"wp-block-list\">\n<li>Inline text<\/li>\n\n\n\n<li>List inside of paragraph<\/li>\n\n\n\n<li>Bullet list<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Templates and Merged documents stored in local database<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Pages<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<!--nextpage-->\n\n\n\n<h2 class=\"wp-block-heading\">Files<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Directory Structure<\/li>\n\n\n\n<li>main.py<\/li>\n\n\n\n<li>main.py<\/li>\n\n\n\n<li>word_file_app.py\n<ul class=\"wp-block-list\">\n<li>word_file_app.py &#8211; library import<\/li>\n\n\n\n<li>word_file_app.py &#8211; create_application<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Directory Structure<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"315\" height=\"1024\" src=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/directory_tree-315x1024.png\" alt=\"\" class=\"wp-image-868\" srcset=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/directory_tree-315x1024.png 315w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/directory_tree-92x300.png 92w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/directory_tree.png 385w\" sizes=\"auto, (max-width: 315px) 100vw, 315px\" \/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">main.py<\/h2>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\"main.py\" ># main.py\n\n# system library --------------------------------------------------------------\nimport os, sys\n\n# local library --------------------------------------------------------------\nfrom    config.flask_app    import FlaskOptions\nimport  word_file_app\n\n\ndef main(argv:list) -&gt; None:\n    web_port = FlaskOptions.API_PORT_PROD.value\n    # create the flask app\n    app = word_file_app.create_application(\n        app_name=__name__\n    )\n\n    app.run(host='127.0.0.1', port=web_port,debug=True)\n\nif __name__ == '__main__':\n    main(sys.argv[1:])<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Import some standard libraries, os and sys. They will be used later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next import our configuration file for the flask app, and our word_file_app which will talk about below.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because we are just running the app locally we will set it to 127.0.0.1, in the config file you can set the port you would like to run on, I happen to use 7002.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">word_file_app.py<\/h3>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\"word_file_app.py\" ># \/word_merge_app.py\n\n# system library -------------------------------------------------------------\nimport os\n\n# packages -------------------------------------------------------------------\nfrom flask          import Flask, send_from_directory, render_template, request\nfrom flask          import send_file, redirect, url_for, Blueprint\nfrom flask          import session\nfrom werkzeug.utils import secure_filename\n\n# local library --------------------------------------------------------------\nimport config.app_config        as app_config\n\n# routes ---------------------------------------------------------------------\nfrom routes.main_app            import base_blueprint\nfrom routes.maintain_file       import maintain_blueprint\nfrom routes.merge_file          import merge_blueprint\nfrom routes.generate_file       import generate_blueprint\n\n# from routes.processing          import processing_blueprint\n\n\ndef create_application(app_name):\n    app = Flask(app_name)\n    app.config.from_object(app_config)\n    app.secret_key ='RandomSecretKey'\n\n    # register blueprints\n    app.register_blueprint(base_blueprint)\n    app.register_blueprint(maintain_blueprint)\n    app.register_blueprint(merge_blueprint)\n    app.register_blueprint(generate_blueprint)\n\n\n    @app.context_processor\n    def inject_title():\n        path = request.path.strip('\/')\n        title = path.capitalize() if path else 'Word Document Merge'\n        return dict(title=title)\n\n    # ------------------------------------------------------------------------\n    # return the app\n    # ------------------------------------------------------------------------\n    return app<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Ok, now we will set up the flask app.  We will import some standard items from the flask library, next our configuration file for this app, and lastly the different route files to be used in the flask blueprints.  We will review all of those later on. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now let&#8217;s go line by line.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">word_file_app.py &#8211; library import<\/h3>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\" word_file_app.py - libray\" ># system library -------------------------------------------------------------\nimport os\n\n# packages -------------------------------------------------------------------\nfrom flask          import Flask, send_from_directory, render_template, request\nfrom flask          import send_file, redirect, url_for, Blueprint\nfrom flask          import session\nfrom werkzeug.utils import secure_filename<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Importing OS standard library. Next from flask package we are importing the main Flask web handler, functions for sending files, functions for redirect webpages, blueprints, and the all important session.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\" word_file_app.py - config\" ># local library --------------------------------------------------------------\nimport config.app_config        as app_config<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The next section imports the configuration file for the app, we will cover the settings in the configuration file later on.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\" word_file_app.py - routes\" ># routes ---------------------------------------------------------------------\nfrom routes.main_app            import base_blueprint\nfrom routes.maintain_file       import maintain_blueprint\nfrom routes.merge_file          import merge_blueprint\nfrom routes.generate_file       import generate_blueprint<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We are going to use four different route files.  The reason being is that there really are four separate steps in creating the merged word document.  Separating out into different blueprints allows us to keep the logic a little more contained. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">routes.main_app, routes.maintain_file, routes.merge_file, routes.generate_file<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">word_file_app.py &#8211; create_application<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\" word_file_app.py - create_application\" >def create_application(app_name):\n    app = Flask(app_name)\n    app.config.from_object(app_config)\n    app.secret_key ='RandomSecretKey'\n\n    # register blueprints\n    app.register_blueprint(base_blueprint)\n    app.register_blueprint(maintain_blueprint)\n    app.register_blueprint(merge_blueprint)\n    app.register_blueprint(generate_blueprint)\n\n\n    @app.context_processor\n    def inject_title():\n        path = request.path.strip('\/')\n        title = path.capitalize() if path else 'Word Document Merge'\n        return dict(title=title)\n\n    # ------------------------------------------------------------------------\n    # return the app\n    # ------------------------------------------------------------------------\n    return app<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">In the main create_application function, we are doing the normal thing you would expect, we are creating the flask application, named app.  The function is called from main.py<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next we read in the app_config file, and set the application configuration settings.  We don&#8217;t have many configuration settings, but allows for future enahancements.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next we register the blueprints. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">routes.main_app, routes.maintain_file, routes.merge_file, routes.generate_file<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This allows for all the different endpoints to be available to the application.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\"word_file_app.py - inject_title\" >    @app.context_processor\n    def inject_title():\n        path = request.path.strip('\/')\n        title = path.capitalize() if path else 'Word Document Merge'\n        return dict(title=title)<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The inject_title function is a shortcut way of adding the page title to each of the html template pages we will use later on. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And lastly we return the app to the main.py.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next we will go through the configuration pages, and how they are used.<\/p>\n\n\n\n<!--nextpage-->\n\n\n\n<h2 class=\"wp-block-heading\">config<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Configuration files for the app.  We will put all of the config settings into and Enum class for easier reference.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Configuration Files\n<ul class=\"wp-block-list\">\n<li>app_config.py<\/li>\n\n\n\n<li>document_style.py<\/li>\n\n\n\n<li>flask_app.py<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">app_config.py<\/h3>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\"app_config.py\" ># \/config \/ app_config.py\n\n# system library --------------------------------------------------------------\nfrom enum import Enum\n\nclass AppConfig(Enum):\n    # version\n    VERSION = \"1.1.0\"\n\n    DATABASE = 'database\/word_file_app.sqlite'<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Import the standard Enum class<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>VERSION <\/strong>is just for reference, value is not that key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>DATABASE<\/strong> is the relative location from the base directory, it should be a subdirectory inside of the app folders.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"document_style_py\">document_style.py<\/h3>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\"document_style.py\" ># \/config \/ document_style.py\n\n# system library --------------------------------------------------------------\nfrom enum import Enum\n\nclass DocumentStyle(Enum):\n    # DOCUMENT\n    TEMPLATE_NAME           = 'word_documents\/cl_template.docx'\n    OUTPUT_DIRECTORY_WORD   = 'download'\n    OUTPUT_DIRECTORY_PDF    = 'download'\n    OUTPUT_FILE_NAME        = '_FileName_'\n    OUTPUT_FILE_PREFIX      = 'output_prefix'\n    OUTPUT_FILE_USE_PLACEHOLDER_VALUE   = True\n    OUTPUT_FILE_PLACEHOLDER_TO_USE      = 'Company Name'\n\n    # DATE FORMAT\n    DATE_FORMAT             = '%d %B %Y'\n    FILENAME_DATE_FORMAT    = '%Y-%m-%d'\n\n    # PAGE LAYOUT\n    PAGE_MARGIN_TOP         = 0.75\n    PAGE_MARGIN_BOTTOM      = 0.50\n    PAGE_MARGIN_LEFT        = 0.75\n    PAGE_MARGIN_RIGHT       = 0.75\n\n    # PARAGRAPH LAYOUT\n    PARAGRAPH_FONT_NAME     = 'Times New Roman'\n    PARAGRAPH_FONT_POINT    = 12\n\n    # PARAGRAPH BULLET\n    BULLET_FONT_NAME        = 'Times New Roman'\n    BULLET_FONT_POINT       = 12\n    BULLET_STYLE            = 'List Bullet 2'\n    BULLET_CHARACTER        = '-'\n\n    # PARAGRAPH\n    CL_P_BREAK              = \"\\n\"<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The following are the document options when creating the Microsoft Word document. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Most of the options are self explanatory.  And for most part, the default values in this file should work.  <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The only ones that might need adjustment are <\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><strong>OUTPUT_FILE_USE_PLACEHOLDER_VALUE<\/strong>: True<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><strong>OUTPUT_FILE_PLACEHOLDER_TO_USE<\/strong>: &#8216;Valid_Placeholder_Name&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\">If generating the Microsoft Word file to download you want to automatically put the value from one of the defined placeholders in the template file, you can set the <strong>OUTPUT_FILE_USE_PLACEHOLDER_VALUE<\/strong> can set the value to True, and in the <strong>OUTPUT_FILE_PLACEHOLDER_TO_USE<\/strong> set the value to a placeholder in the document.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When working with <strong>DATE_FORMAT <\/strong>and <strong>FILENAME_DATE_FORMAT<\/strong> configuration, you can reference, the appropriate codes here.  <a href=\"https:\/\/docs.python.org\/3\/library\/datetime.html#strftime-and-strptime-format-codes\" target=\"_blank\" rel=\"noreferrer noopener\">Python String Format Codes<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"flask_app_py\">flask_app.py<\/h3>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:python decode:true \" title=\"flask_app.py\" ># \/config \/ flask_app.py\n\n# system library --------------------------------------------------------------\nfrom enum import Enum\n\nclass FlaskOptions(Enum):\n    # Used to connect to sqlite3 database\n    SESSION_TYPE = \"sqlalchemy\"\n                                \n    SESSION_SQLALCHEMY_TABLE = 'sessions'\n\n    #port to run flask on\n    API_PORT_DEBUG =\"7000\"\n    API_PORT_TEST = \"7001\"\n    API_PORT_PROD = \"7002\"\n\n<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Here are the configuration settings that are passed to the flask application.  The key one is to define how we will store the SESSION variable information.  Using sqlalchemy and table name of sessions, seems to be the most common way of doing it.  Since the app is running locally, I wouldn&#8217;t be overly concerned about the settings here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I set up debug, test, prod port numbers, most likely you would only run the app on the PROD port number.<\/p>\n\n\n\n<!--nextpage-->\n\n\n\n<h2 class=\"wp-block-heading\">Database<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before we get into the app code, I want to review how the database is set up. Keeping the database in mind will make things easier when we are discussing some of the application logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Database Diagram<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"663\" src=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image-1024x663.png\" alt=\"\" class=\"wp-image-875\" srcset=\"https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image-1024x663.png 1024w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image-300x194.png 300w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image-768x497.png 768w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image-900x583.png 900w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image-1280x829.png 1280w, https:\/\/eipsoftware.com\/musings\/wp-content\/uploads\/2025\/05\/image.png 1391w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">There are four tables.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">template, placeholder, replacement, and generated. Templates is the main table with a foreign key (1:n) to each of the other tables.  Replacement has an additional foreign key (1:n) with the placeholder table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The ddl to create the tables is in the query folder, in the database_maintenance.py file.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">template<\/h2>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >template_id     int         id number for the template\ntemplate_name   varchar     name of the template\ntemplate_file   blob        Microsoft Word Template\ndeleted         int         did user ask to delete template file\nmodify_at       timestamp   date time row was last updated\n<\/pre><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">placeholder<\/h2>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" title=\"table placeholder\" >placeholder_id     int         id number for the placeholder\ntemplate_id        int         foreign key to template.template_id\nplaceholder_type   varchar     type of placeholder, valid are static, inline, list, bullet\nplaceholder_name   varchar     name of placeholder\nplaceholder_order  int         order the placeholder appears in template\ndeleted            int         did user ask for the placeholder to be deleted\nmodify_at          timestamp   date time row was last updated\n<\/pre><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">replacement<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">generated<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<!--nextpage-->\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This app will use a web interface to facilitate merging content into a Microsoft Word file. It is the equivalent of the mail merge features with some notable improvements. The repo is available on github at github &#8211; word_file_merge_app<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_crdt_document":"","footnotes":""},"categories":[3,10],"tags":[],"series":[],"class_list":["post-867","post","type-post","status-publish","format-standard","hentry","category-python","category-javascript"],"_links":{"self":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts\/867","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/comments?post=867"}],"version-history":[{"count":9,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts\/867\/revisions"}],"predecessor-version":[{"id":882,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/posts\/867\/revisions\/882"}],"wp:attachment":[{"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/media?parent=867"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/categories?post=867"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/tags?post=867"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/eipsoftware.com\/musings\/wp-json\/wp\/v2\/series?post=867"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}