{"id":19747792,"url":"https://github.com/haasr/collaborative-blog","last_synced_at":"2026-05-06T03:34:36.187Z","repository":{"id":44406854,"uuid":"497169069","full_name":"haasr/collaborative-blog","owner":"haasr","description":"Collaborative Django blog / CMS","archived":false,"fork":false,"pushed_at":"2022-10-27T18:07:07.000Z","size":41061,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-23T22:14:25.634Z","etag":null,"topics":["blog","blogging-application","blogging-site","blogging-system","cms","content-management-system","django","django-application","django-framework","django-project","python","python3"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/haasr.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-05-28T00:00:53.000Z","updated_at":"2023-07-11T20:33:16.000Z","dependencies_parsed_at":"2023-01-20T08:48:52.151Z","dependency_job_id":null,"html_url":"https://github.com/haasr/collaborative-blog","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haasr%2Fcollaborative-blog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haasr%2Fcollaborative-blog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haasr%2Fcollaborative-blog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haasr%2Fcollaborative-blog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/haasr","download_url":"https://codeload.github.com/haasr/collaborative-blog/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241078510,"owners_count":19905873,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blog","blogging-application","blogging-site","blogging-system","cms","content-management-system","django","django-application","django-framework","django-project","python","python3"],"created_at":"2024-11-12T02:18:55.893Z","updated_at":"2025-10-12T18:14:19.178Z","avatar_url":"https://github.com/haasr.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"*******************\r\ncollaborative-blog\r\n*******************\r\n\r\nCollaborative Django blog app with built-in CMS.\r\n\r\nExample Sites\r\n#############\r\n\r\nhttps://ryansreflections.herokuapp.com/\r\n\r\nhttps://littlelearnersresources.com/\r\n\r\n.. image:: ./readme_images/showcase-1-home.png\r\n    :width: 800\r\n    :alt: Showcase home page in desktop and mobile views\r\n\r\n\r\n.. image:: ./readme_images/showcase-2-topics-timeline.png\r\n    :width: 800\r\n    :alt: Showcase Topics and Timeline pages\r\n\r\n\r\n.. image:: ./readme_images/showcase-3-posts.png\r\n    :width: 800\r\n    :alt: Showcase Posts pages\r\n\r\n\r\n.. image:: ./readme_images/showcase-4-admin.png\r\n    :width: 800\r\n    :alt: Showcase site administration menu and Mail Send Failures page\r\n\r\n\r\nThis app emerged from a rough version which hosted my original blog. It is now\r\nmuch more fully featured, offering configurability of each of the public site\r\npages that can be made visible, including the ability to enable multiple forms\r\nto collect contact or contributor information and to allow viewers to subscribe\r\nto automated newsletters.\r\n\r\nIt features a rich backend site where staff can author, collaborate on, and\r\nmanage posts, while admins can do all the good admin stuff to keep the site's\r\nmedia fresh and current, manage newsletter and form features, and manage user\r\naccounts and permissions.\r\n\r\nAll this verbosity below before the setup instructions is to provide detail on\r\nthe thinking behind different features, and how components of the blog application\r\nwork and can be used.\r\n\r\n.. contents:: Contents\r\n\r\nThe Source Code\r\n##################\r\n\r\nThe source code is in need of some refactoring, particularly, I think that all\r\nthe classes and modules related to sending emails should be focused into one\r\napp within the project, rather than split between some modules in ``site_pages_forms``\r\nand ``mail_subscription``. I have also noticed that the models and forms modules in\r\n``admin_pages`` have gotten very long since the inception of my code base, and while\r\nModels like ``Comment`` and ``Thread`` are only presented in their respective\r\nModelForms in the site_pages, they are still related directly to the ``Post`` object,\r\nwhich is why the models and forms for them were placed in the ``admin_pages`` model.\r\n\r\nLooking back, I probably would have split up the apps differently to avoid\r\nareas of overlap between the apps (where ``site_pages.views`` has to import models\r\nand forms from ``admin_pages``, for example), but I developed the basis of this blog\r\nvery quickly in about 2 weeks and have been building off of it ever since so...\r\nsunk cost and all that. I still think the overall project is clean enough to\r\ncontinue developing on.\r\n\r\nStatic Files and Database\r\n############################\r\n\r\nStatic files are stored in an AWS S3 bucket where a specific hierarchy of\r\nfolders exists to serve the CSS, JavaScripts, and the many images related\r\nto the public pages, to posts, and to profile images. The database is configured\r\nto maintain a remote Postgres connection. The separation of static files and the\r\ndatabase from the application code was inspired by my experience with developing\r\nmicroservices architectures. Using these external services not only allows for\r\nvery cheap static files storage but it increases the modularity of the blog,\r\nmaking it easier to re-deploy elsewhere (without the need to roll a new database\r\nand perform ETL, for example). The database could easily be swapped out with\r\nsomething other than Heroku by changing the default database configuration\r\nin ``blog/settings.py`` although changing from AWS S3 to another form of static\r\nfiles storage would require a careful rewrite of many functions in ``admin_pages/views.py``,\r\n(to use FileSystemStorage instead of my S3Upload models, for example) and perhaps\r\nsome lines in other modules.\r\n\r\nEmail\r\n########\r\n\r\nThis app was configured to use the transactional email service, Sendinblue. I\r\nuse the free plan which allows sending up to 300 emails per day and I find its\r\ntemplates to be very natural to create (because they basically use a subset of\r\nDjango's template tag language). I will provide example email templates that\r\ncan be created to display emailed form data and newsletters.\r\n\r\nHow it Works\r\n------------\r\n\r\nAfter the app is set up, an ``EmailAccount`` object of ID 1 exists (from ``admin_pages/models.py``).\r\nThe email address provided to this model (which is set in the administrative menu,\r\nin Accounts \u003e Email, or ``\u003csite_url\u003e/admin_pages/manage_email_account/``) will be\r\nused as the recipient of submitted form data. This address can be separate from\r\nthe one used by Sendinblue.\r\n\r\nSendinblue\r\n**********\r\n\r\nDevelopers can `get an API key \u003chttps://help.sendinblue.com/hc/en-us/articles/209467485-What-s-an-API-key-and-how-can-I-get-mine-\u003e`_\r\nfor their Sendinblue accounts. Note that the `django-anymail documentation \u003chttps://anymail.dev/en/stable/esps/sendinblue/\u003e`_\r\nsays that that only v3 keys (and not v2 keys) will work with Sendinblue. This\r\nAPI key gets set as the environment variable, \"BLOG_SENDINBLUE_API_KEY\", so\r\nDjango can use the associated account with the SMTP backend to send the emails.\r\n\r\nThe next step would be to configure email templates that can parse the data\r\nemailed (i.e., templates for the following: Subscribed Confirmation, Newsletter\r\n(Featured only),  Newsletter (Recent only), Newsletter (Featured \u0026 Recent),\r\nNew Contributor Request, Email Contact Request, and Contact Request). Of course,\r\nyou only need templates for the forms you plan to use (if you toggle off the\r\nSubscribe form and the Email Contact form, you would not need those templates).\r\nThe templates are given unique integer IDs which appear to the left of their\r\nnames in the table on Sendinblue's ``Templates`` tab (they should be marked active\r\ntoo).\r\n\r\nRobustness to identify and react to failures\r\n********************************************\r\n\r\nWhen the app fails to send an email, it saves the name of the form, the to address,\r\nthe time of the failure, and the form data which can be viewed through the\r\nadministrative menu \u003e Failures \u003e Mail Send Failures (``\u003csite_url\u003e/admin_pages/failures/mail_send_failures/30/``).\r\nThis page offers visualizations of instances where form data and newsletters did not\r\nsend, separately for the given number of days (default = 30: The current day and the\r\nprevious n-1 days).\r\n\r\nThe view allows attempting to resend the form data for each\r\nform (but not for newsletters, at the moment). For example, if a template ID was not\r\nconfigured for the Contact form, an Exception will be shown saying \"Invalid template id\"\r\nfor the unsent form data. This informs the admin to enter a valid template ID\r\nin the Contact form settings. Then when they click \"Resend\", the form data may\r\nbe sent successfully.\r\n\r\nThe mail send failures view is ideal for identifying common (or rare) exceptions and\r\nit ensures that admins can have have a backup record of the forms submitted to them\r\nso the submitted form data is not just lost.\r\n\r\nUsing another transactional email service\r\n-----------------------------------------\r\n\r\nI suppose the email config could be changed in ``blog.settings`` to use another\r\ntransactional email service that ``django-anymail`` supports, assuming it uses\r\ninteger IDs for its email templates (which is an integral factor in how my\r\napp sends form data). I do not know how rich the template languages are with other\r\nproviders or if the parameters sent from this app can be parsed by other providers\r\n(I venture to guess that they can, but do your research).\r\n\r\nAll of that is to say, if you plan to use a service other than Sendinblue, be\r\nprepared to tweak the code until you are able to get it sending data that can\r\nbe correctly parsed in your email templates, and be prepared for new exceptions,\r\nalthough many exceptions...but not all\r\n( see https://github.com/pinax/django-mailer/issues/73 )...can be captured by setting\r\n``fail_silently`` to ``False`` when calling the ``send`` function of a ``django.core.mail.EmailMessage``\r\ninstance.\r\n\r\nThe Newsletter Feature\r\n#########################\r\n\r\nThe newsletter feature of this site is in an experimental state, due to my\r\nlimitations in being able to test it. I host my blog using a version of this\r\ncode on a free Heroku Dyno. It is worth noting that the newsletter feature will\r\nNOT work as-is on a free Heroku implementation because the Dyno's resources are\r\nput to sleep after ~30 minutes of inactivity. That means that my scheduler\r\n(in ``mail_subscription/newsletters.py``) is liable to be interrupted and would not\r\nsend newsletter emails at the appropriate time. A possible work-around would be\r\nto write a loop in the scheduler to make an HTTP request to an endpoint of the\r\nweb app every 25 minutes to keep it alive. I chose not to do that for now.\r\n\r\nA developer with some Django experience could also rewrite the scheduling to\r\nuse Django base commands along with a scheduler such as Heroku Scheduler which\r\ncan run cron-like jobs by spinning up one-off Dynos. If you are a developer\r\ninterested in doing that, feel free to reach out to me and I'll at least look\r\ninto helping develop some mechanisms for scheduling that can play well with\r\nHeroku without breaking the ability to schedule newsletter options through the\r\nadministrative GUI.\r\n\r\nWhile I expect the newsletter feature to work consistently on ordinary web servers,\r\nI have yet to test it outside of my local environment. I have recently deployed an\r\nimplementation of this web app on an Ubuntu web server where I have employed the\r\nnewsletter feature which should allow me to assess the scheduler's viability and\r\nmake changes, if needed.\r\n\r\nUsers, Authors, Posts, and Collaborators\r\n###########################################\r\n\r\nUsers, Authors\r\n--------------\r\n\r\nA User account (for login) is inextricably linked to an AuthorProfile account in\r\na 1:1 relationship. This design was to separate concerns between the user\r\ninformation needed for authentication and administration (username, real name,\r\nand email address) and the public-facing profile information that an author may\r\nwish to display (preferred name, profile icon, and a bio). When a user is signed\r\nin, they can find their 'Account Settings' dropdown menu in the top right\r\nidentified by their profile icon. In those settings \"Author Profile\" allows the\r\nuser to configure what public readers will see when they read one of their public\r\nposts while \"User Account\" refers to the settings for the information used to\r\nauthenticate (including two-factor authentication) and the information that admins\r\ncan see (First \u0026 Last name and email address). It is **important** that each user\r\n**specifies an email address they have access to** because their listed address is\r\nused to facilitate password reset.\r\n\r\n\r\nWhen is a user account created?\r\n*******************************\r\n\r\nIn my configuration, user accounts are only ever manually created by an admin: me.\r\nThe purpose of the Contribute form on my site (currently https://ryansreflections.herokuapp.com/contrib/)\r\nis to identify prospective contributors. If a contact seems like someone worth\r\ngiving access to post on my blog (currently no one, because I'm pretty sure no one\r\nreads my blog), I will take their desired username, first name, last name, and\r\nemail address, and use that information to create an account for them.\r\n\r\nMy process after receiving an email with an instance of 'Contribute' form data is as\r\nfollows:\r\n\r\n1) Create a user account from the admin side using the info provided in the form.\r\nSet some bogus password (I should write a password generator on the account\r\ncreation view).\r\n\r\n2) Send the user a personal email detailing their username and email, where to login,\r\netc., and a link to the password reset page (https://ryansreflections.herokuapp.com/users/reset_password/).\r\n(I may eventually automate the sending, if not the composing, of such an email too).\r\n\r\nThen the user's process upon receiving my email is:\r\n\r\n1) Visit the password reset link, enter the email address associated with the\r\naccount, follow the reset link emailed, and follow the reset steps on the site.\r\n\r\n2) At the end of password reset, the user is prompted to log in. After logging\r\nin, they will be asked to configure a two-factor authentication (2FA) option. The\r\nuser will need a smart mobile device to install an authentication app such as\r\nGoogle Authenticator. Once installed, they need to scan the provided QR code to\r\nset up the 2FA. Then they will have to enter the 2FA token in order to complete\r\nsign-in.\r\n\r\nWhy can't viewers register accounts?\r\n------------------------------------\r\n\r\nMy answer to this question is multifaceted:\r\n\r\n1) I didn't feel like it. This is the main reason. A viewer can already subscribe to\r\nreceive email newsletters and commenting is open to everyone. Giving a viewer an account\r\nwould mean that the account *does* something extra for them -- maybe they could have a\r\nprofile and their screen name and profile image could show up on their comments (right now,\r\nif an author comments, their preferred name is used). Or maybe they could have a little\r\nview where they can access their favorited posts. But making entirely new functionality\r\nso someone can have a profile pic in the comments or so that someone can effectively do\r\nwhat their browser's bookmark tool can do is not worth my time.\r\n\r\n2) It presents an extra governance problem. There are more accounts of people\r\nthat you, as an admin, don't know. Some could have bad intentions. Many more\r\ncould just be forgetful or stupid, lock themselves out, and fill admins' inboxes\r\nwith messages for assistance. The governance problem is also characterized by\r\nhaving more people's data. If I extened the app so that anyone could register an\r\naccount, I would plan for the future and expect a large quantity of users demanding\r\nmore account information be stored, primarily to make a feasible account recovery\r\nprocess. I am very much a minimalist when it comes to storing personal information; my goal\r\nis to do as much as possible for the user experience with as little information as\r\npossible. At this time, I am not interested in collecting or storing a considerably\r\ngreater, and probably more detailed, volume of personal information that could come\r\nwith free account registration.\r\n\r\n3) I've touched on this, but letting viewers register accounts does not accomplish much.\r\nMy suspicion is that most viewers will be casual. Frequent viewers who really want to\r\nget involved in the community can simply contact the admin through the Contribute page\r\nto get an account and contribute as an author. As the blog expands, trusted members who\r\nthe admin is acquainted with (hopefully well acquainted with) may be promoted to admin\r\nto help manage the website. The candidate contributors do need to be vetted, at least\r\nlosely, to ensure their intentions and their writing skills. If there is a bottleneck\r\nin people getting accounts, that is also a soft check against those who requested to\r\nbe a contributor who are not particularly dedicated to the blog's community.\r\n\r\n\r\nOK, but I, as a developer, want to let viewers register accounts...What is the easiest way to go about it?\r\n----------------------------------------------------------------------------------------------------------\r\n\r\nI have left the blog open to be easy to allow for viewers to register user accounts\r\nfor possible future growth although, as you can read above, I am not compelled to\r\ndo that myself right now. If you wanted to transform this blog, to say, have a very\r\nengaging social media-type community, you may want to let users register their own\r\naccounts. Currently, standard accounts are really staff accounts (meaning ``user.is_staff == True``).\r\nAll users with staff (and not superuser) permissions have access to their user profile,\r\ntheir author profile, and the ability to manage the posts they author (Create, Read,\r\nUpdate, \u0026 Delete posts they author). They can also manage posts that they collaborate\r\non.\r\n\r\nAdmins are accounts that not only have the ``staff`` permission but the ``superuser``\r\none as well. With that, they can manage...everything. The site look, the content\r\nof pages, which pages are accessible and visible to the public, the newsletter,\r\nall other user accounts, an email denylist for spammers, and configure SEO.\r\n\r\nThat means a user without superuser or staff permission could be designated as\r\na \"regular\" user who has some type of profile access that does not allow them\r\nto manage posts or other more elevated privelege. You could use a similar method\r\nto my register method found in the ``users.views`` module, omitting the ``is_staff``\r\nassignment. At this point, I would consider using groups to designate types of\r\nusers to make permissions easier to assign and revoke (you might have 3 types\r\nof users but one day you might have 4, then 5, so future-proofing is never bad).\r\n\r\nIf you want just any rando to be able to become an author on the blog (some kinda\r\nanarchist blog), you could simply put up a registration page and link it in the\r\nmain navigation. The registration page would be just like the one I have used for\r\ncreating new users on the admin side, sans the \"Is admin\" checkbox. Then they\r\nwould get to create their own accounts. If you are an anarchist or a die-hard\r\nlibertarian interested in this ability to offer an underregulated free-speech blog\r\nplatform, I'd be happy to spend a few hours developing/designing it for you (in\r\nother words, spend 10 minutes developing and like 200 minutes making the HTML look\r\nright!).\r\n\r\n\r\nTwo-Factor Authentication\r\n#########################\r\n\r\nWhat? Why?\r\n----------\r\n\r\nMy app has recently been configured to use 2FA tokens as a mandatory method for\r\nusers to have access to their accounts and the staff side of the site. From a\r\nsecurity perspective, 2FA should be mandatory in 2022, even at the expense of\r\nconvenience. So a developer could technically gut all the two-factor stuff from\r\nthe app, point the login URL back to my original login view in the users app,\r\ntweak a few of my ``admin_pages`` templates and successfully use the site without\r\n2FA, but nobody would be winning in that scenario: accounts would all be vulnerable\r\nbecause of phishing attempts (do not underestimate the stupidity of any of your\r\nusers).\r\n\r\nWhile there are certainly more convenient means than token generation, it is the\r\nmost reliable, not depending on the smart device to have any Internet connection.\r\nIt might be nice to have push notifications provided by the authenticator app and\r\nuse the OTP tokens as a backup option, but I'm too lazy to do that. Nobody pays me :)\r\n\r\nResetting 2FA\r\n-------------\r\n\r\nUsers can always reset their own 2FA from their own account settings through\r\n``Account Settings`` \u003e ``User Account`` \u003e ``2FA Settings`` \u003e ``Reset Two-Factor Authentication``.\r\nAfter that is done, they are immediately asked to configure a new 2FA method before they\r\ncan get back into the staff side of the site. Of course there is an obvious problem here:\r\nIf a user cannot *use* their 2FA method anymore, they cannot finish logging in to reset it\r\n(a common example in my institution is when a user gets a new phone, haphazardly thinking\r\nthat their OTP codes will magically transfer to their new phone, which is an understandable\r\nexpectation given the way most app data transfers seemlessly). This is **why it is essential\r\nfor users to store their backup tokens**.\r\n\r\nBackup tokens are also found on the 2FA Settings page\r\n(``Account Settings`` \u003e ``User Account`` \u003e ``2FA Settings`` \u003e ``Show Tokens``). They should store\r\nthese somewhere where they are (1) secure and (2) easy to locate. **It is strongly recommended**\r\nto **instruct users to save these tokens** after setting up their accounts.\r\n\r\nDesired Improvement\r\n*******************\r\n\r\n**TL;DR**:\r\n\r\nI will probably improve the capability of resetting 2FA by giving admins the ability\r\nto reset 2FA on behalf of users **verbally** requesting it. In order to avoid undermining\r\nsecurity, admins will need to verify two pieces of personal information, again, **verbally**,\r\nbefore fulfilling the request for 2FA reset (to securely confirm identity). The personal\r\ninformation will not be mandatory to store on the site with the condition that admins will\r\nonly be able to do a reset contingent on there being personal information for a user that\r\ncan be verified.\r\n\r\n\r\nThe current 2FA setup is workable, but it still is not quite ready for institutional use,\r\nto me, because the user can still be locked out of their account (if they don't have access\r\nto their tokens). I will likely be prioritizing an administrative capability to reset 2FA\r\nfor the user so that they will be able to get to the 2FA configuration prompt. This will\r\nnecessitate more user account information, however, to avoid undermining the security (e.g.,\r\nany user could claim they are the account owner and request 2FA reset; that threat is \r\nabsolutely critical because if a user's email account was compromised, the hacker could\r\nimpersonate them by sending from the account and once 2FA is reset, all the hacker would\r\nneed to do is set up their own 2FA method using the account, because if the hacker is in\r\ncontrol of the email account, they are able to reset the user's password on the blog).\r\n\r\nIn the U.S., the last four digits of someone's Social Security Number (SSN) is one personally\r\nidentifiable (and still ubiquitously actually known) bit of information. More universally,\r\neveryone knows their date of birth. My plan, at least in abstract, is to put form fields\r\nin the User account settings form to enter last 4 digits of SSN (if applicable), date of\r\nbirth, and a challenge question from a fixed set of possible questions. It will be on the user\r\nto enter this information after they initially set up 2FA on their account.\r\n\r\nIf the user contacts me or another admin, asking us to reset 2FA, our first recommendation\r\nwill be for the user to use a backup token and reset on their end. If that is not possible,\r\nthe admin will be required to verify either date of birth AND either SSN or the challenge\r\nquestion (date of birth alone is woefully inadequate). These should ONLY be verified over\r\nthe phone or secure teleconference (or in-person if they have the luxury). If the user\r\ncannot provide the necessary personally identifiable information, they should not be granted\r\nthe reset since we cannot prove their identity.\r\n\r\nLastly, because of the context of this blog, many users may not feel comfortable storing such\r\npersonal information in the site. Understandably so. If a user happens accross my blog, likes\r\nwhat they read, and thinks \"I could contribute to this blog\", they may fill out my contribute\r\nform and get setup to write posts on my blog, but they will have never met me in person, nor\r\nwill they have had a previous history with me. If I make it mandatory for them to enter their\r\npersonal info., they may very well say \"forget it\" (rightfully so; I wouldn't provide such\r\ninformation unless I had a personal relationship with the site admin). So the personally\r\nidentifiable information should be optional, but it should be clear to users that if they do\r\nnot provide the PII and they somehow lose their backup tokens and cannot use 2FA, we will NOT\r\nbe able to help them get into their account.\r\n\r\nPosts, Collaborators, and Topics\r\n###################################\r\n\r\nAll posts can have a splash image, 1:N topics, one and only one main author (associated by\r\nAuthorProfile), and 0:N collaborating authors. Collaborators have the same permissions to\r\nthe post as the original author, sans the ability to delete the post or manage collaborators.\r\nIf an author who is also a site admin is added as a collaborator on a post, the admin will\r\nhave all the same permissions as the original author, including the ability to delete the post\r\nand manage collaborators. All posts have an ``og_date`` field, referring to their original\r\ncreation DateTime timestamp, and a ``date`` field (which I should have named ``date_last_mod``)\r\nindicating the the last modified date and time.\r\n\r\n``Topic`` objects have a ``name`` (e.g., \"Sportsball\") and a ``splash_image``, and can be marked\r\nas featured using the ``is_featured`` boolean. When a topic is marked as featured, it gets\r\nlisted in a large box with its splash image behind it on the topics page. All topics\r\nthat exist (featured or not) are listed as links which can be filtered by name on the\r\ntopics page. Clicking a topic link on the topics page loads a 'topic_posts' view where\r\nposts that include that topic are shown from most recent to oldest, and are searchable.\r\n\r\nServices Required\r\n#################\r\n\r\nI will use free tiers of all the services besides the standard AWS S3 bucket storage.\r\nWhile it is technically not free ($0.023/GB/month for my project; see https://aws.amazon.com/s3/pricing/),\r\nmy monthly costs are so low (fractions of a cent to a cent) that my invoices are waived.\r\n\r\n- AWS S3 standard bucket\r\n- Remote database (I will walk through setting up Postgres on Heroku)\r\n- Sendinblue account\r\n- TinyMCE account\r\n\r\nSetup\r\n######\r\n\r\nSetup will be easiest to follow in the sequence I have written these sections in. For setup,\r\nI recommend using a staging environment and then once everything seems to be working, to\r\nmove the configuration to a production server.\r\n\r\nI will assume that Python is installed and that you can access it from a shell. If not,\r\nthere's this wonderful resource called the World Wide Web that can help. I will be using\r\nPowershell and will leave some examples for Debian-based Linux as well.\r\n\r\nSetup virtualenv\r\n----------------\r\n\r\nFor this project, we want to first set up a virtual environment. This way, we can install\r\ndependencies to this virtual environment rather than our global Python environment. This\r\nwill make it easier to track the dependencies our application uses, and easier to deploy\r\nour project.\r\n\r\n1. First, open your terminal to the main folder of this cloned repository and make sure you\r\nhave the virtualenv package installed:\r\n\r\n``pip install --user virtualenv```\r\n\r\nIn Ubuntu-based distributions, you can install it using:\r\n\r\n``sudo apt install python3-venv``\r\n\r\n2. Create the virtualenv (still in the main repo folder):\r\n\r\n``python -m venv venv``\r\n\r\n3. Activate it.\r\n\r\n3a. In PowerShell:\r\n\r\n``.\\venv\\Scripts\\activate``\r\n\r\n3b. In Linux:\r\n\r\n``source .venv/bin/activate```\r\n\r\nTo deactivate it (when you want to use your user Python environment), simply type\r\n``deactivate``.\r\n\r\n4. Install the requirements.\r\n\r\n``pip install -r requirements.txt``\r\n\r\n(You will have to use ``pip3`` in Linux)\r\n\r\n\r\nHeroku Postgres Database Setup\r\n------------------------------\r\n\r\nHeroku: Free Tier Ending Soon\r\n*****************************\r\n\r\nUnfortunately, Heroku has recently announced that free Dynos and Heroku Postgres databases will no\r\nlonger be available starting on November 28, 2022. These will need to be upgraded to avoid disruption\r\nand deletion of the free-tier services.\r\n\r\nCreate the database\r\n*******************\r\n\r\nFirst, we will set up a remote database. Of course, you do not have to use Heroku or even Postgres\r\nto host the database, but it is what I will use in this example.\r\n\r\n1. If you don't have an account, make a free one and sign in.\r\n\r\n2. After you are signed in, create a new app. Name it whatever you'd like.\r\n\r\n.. image:: ./readme_images/heroku-1-create-app.png\r\n    :alt: Heroku app creation screen with app name entered.\r\n\r\n\r\n3. Click the **Resources** tab and search \"postgres\" in the Add-ons search bar.\r\n\r\n.. image:: ./readme_images/heroku-2-search-resources.png\r\n    :alt: Resources search bar with term postgres entered\r\n\r\n\r\n4. Select **Heroku Postgres** and choose your tier. I'm using the Hobby-Dev one.\r\n\r\n5. Now click on the link to your database where it appears under Add-ons.\r\n\r\n6. In the new tab, click **Settings**. And then click **View Credentials...**\r\n\r\n.. image:: ./readme_images/heroku-3-view-credentials.png\r\n    :width: 800\r\n    :alt: Settings screen with View Credentials button underlined\r\n\r\n\r\nConfigure the project to use the Postgres database\r\n**************************************************\r\n\r\n1. Export the database variables listed on the credentials screen as the following\r\nenvironment variables:\r\n\r\n.. code-block::\r\n\r\n    BLOG_DB_HOST: The Host string\r\n    BLOG_DB_NAME: The Database string\r\n    BLOG_DB_USER: The User string\r\n    BLOG_DB_PORT: The Port string\r\n    BLOG_DB_PASS: The Password string\r\n\r\n\r\nSendinblue Setup\r\n----------------\r\n\r\nRegister a Sendinblue account at ( https://help.sendinblue.com/ ). Then refer to\r\n`their instructions \u003chttps://help.sendinblue.com/hc/en-us/articles/209467485-What-s-an-API-key-and-how-can-I-get-mine-\u003e`_\r\nto obtain an APIv3 key Once you have the key, export it to the variable:\r\n\r\n``BLOG_SENDINBLUE_API_KEY``\r\n\r\n\r\nTinyMCE Setup\r\n-------------\r\n\r\nRegister a TinyMCE account at ( https://www.tiny.cloud/ ). Once you have finished\r\nregistering, click **Dashboard**.\r\n\r\n.. image:: ./readme_images/tinymce-1-dashboard.png\r\n    :width: 600\r\n    :alt: TinyMCE page with Dashboard link underlined in menu\r\n\r\n\r\n1. On the dashboard, scroll down and copy the script. It will look like this:\r\n\r\n``\u003cscript src=\"https://cdn.tiny.cloud/1/\u003cX...\u003e/tinymce/6/tinymce.min.js\" referrerpolicy=\"origin\"\u003e\u003c/script\u003e``\r\n\r\n2. Export this script to the following environment variable:\r\n\r\n``BLOG_TINYMCE_SCRIPT``\r\n\r\nYou will need to escape the script's characters where your export statement (probably\r\nlocated in .bashrc with all your other exports, if on Linux) would look like this:\r\n\r\n``export BLOG_TINYMCE_SCRIPT=\"\u003cscript src=\\\"https://cdn.tiny.cloud/1/\u003cX...\u003e/tinymce/6/tinymce.min.js\\\" referrerpolicy=\\\"origin\\\"\u003e\u003c/script\u003e\"``\r\n\r\nNotice that the string has been wrapped in quotation marks where the double quotes inside\r\nit are escaped with the backslash character.\r\n\r\n3. Click on the **Approved Domains** tab. Verify a a confirmation email if necessary and then\r\nadd \"127.0.0.1\" and your planned site's domain so TinyMCE will correctly work in testing\r\nand production.\r\n\r\n\r\nAWS S3 Setup\r\n------------\r\n\r\nTake a break. Make a cup of coffee. The S3 portion has many steps.\r\n\r\nCreation\r\n********\r\n\r\n1. Try going to this site ( https://aws.amazon.com/console/ ). Click **Create an AWS Account** if\r\nyou don't have an account (unless this has changed from the time of writing, in which case, Google it).\r\n\r\n.. image:: ./readme_images/aws-1-create-acct.png\r\n    :width: 800\r\n    :alt: AWS Console website with create account button underlined\r\n\r\n\r\n2. From the AWS Console screen ( https://aws.amazon.com/console/ ), drop down the **All Services**\r\nmenu and look for S3 under storage. Click it.\r\n\r\n.. image:: ./readme_images/aws-2-click-s3.png\r\n    :width: 800\r\n    :alt: AWS Console All Services menu with S3 underlined under Storage\r\n\r\n\r\n3. Select **Create Bucket**.\r\n\r\n.. image:: ./readme_images/aws-3-create-bucket.png\r\n    :width: 500\r\n    :alt: Buckets screen with Create bucket buttons\r\n\r\n\r\n4. Name the bucket. I am leaving all the other settings as the default. If you know what you're\r\ndoing, change them accordingly. Then click **Create bucket**.\r\n\r\nIf you haven't added a payment option, Amazon might prompt you before you can create the bucket.\r\n\r\nPermissions\r\n***********\r\n\r\n1. If you aren't looking at the **Buckets** screen, navigate to **Amazon S3 \u003e Buckets**.\r\n\r\n.. image:: ./readme_images/aws-4-s3-buckets-page.png\r\n    :width: 800\r\n    :alt: Amazon S3/Buckets screen\r\n\r\n\r\n2. Click your bucket's name under Name and then click the Permissions tab. Scroll to the very\r\nbottom until you see the Cross-origin resource Sharing (CORS) section.\r\nClick Edit and enter the following JSON:\r\n\r\n.. code-block:: json\r\n\r\n   [\r\n        {\r\n            \"AllowedHeaders\": [\r\n                \"Authorization\"\r\n            ],\r\n            \"AllowedMethods\": [\r\n                \"GET\",\r\n                \"POST\"\r\n            ],\r\n            \"AllowedOrigins\": [\r\n                \"*\"\r\n            ],\r\n            \"ExposeHeaders\": [],\r\n            \"MaxAgeSeconds\": 3000\r\n        }\r\n    ]\r\n\r\nSo we are allowing any domain right now by using the star character. Eventually, we will\r\nwant to change this to our website's domain once we are in production, but this will do\r\nfor now.\r\n\r\n2. Navigate to the main AWS Console screen. You can click the AWS icon in the navigation or\r\nre-enter the URL: https://aws.amazon.com/console/\r\n\r\n3. Type \"iam\" in the navigation search bar and click on the IAM option that shows up. In\r\nthe IAM dashboard, click Users in the Access Management menu on the left:\r\n\r\n.. image:: ./readme_images/aws-5-iam-access-mgmt.png\r\n    :width: 700\r\n    :alt: Identity and Access Management menu with Users option underlined.\r\n\r\n\r\n4. Click **Add Users** and we are going to create a new user, giving them a key for\r\nprogrammatic access:\r\n\r\n.. image:: ./readme_images/aws-6-iam-add-users.png\r\n    :width: 800\r\n    :alt: Add User screen with user details and AWS access type options.\r\n\r\n\r\n5. Next, under **Set Permissions**, choose **Attach existing policies directly**. Then type\r\n\"amazons3\" in the search bar to filter the options and tick **AmazonS3FullAccess**.\r\n\r\n.. image:: ./readme_images/aws-7-iam-policies.png\r\n    :width: 800\r\n    :alt: Filter policies view with AmazonS3FullAccess policy selected.\r\n\r\n\r\n6.  Click **Next**. Skip the tags screen and then click **Create user**.\r\n\r\n\r\n7. Download the CSV file containing your credentials.\r\n\r\n.. image:: ./readme_images/aws-8-iam-download-csv.png\r\n    :width: 600\r\n    :alt: Success screen with downloadable CSV file of newly created credentials.\r\n\r\n\r\nConfigure the project to use your S3 bucket\r\n*******************************************\r\n\r\n1. Export the variables listed in your credentials file to the following\r\nenvironment variables:\r\n\r\n.. code-block::\r\n\r\n    AWS_ACCESS_KEY_ID\r\n    AWS_SECRET_ACCESS_KEY\r\n    AWS_STORAGE_BUCKET_NAME\r\n\r\n\r\nThe first two variables are listed in the file and the bucket name can\r\nbe found on the AWS webpage.\r\n\r\n2. Now it is time to upload all the necessary static files in their hierarchy to your\r\nS3 bucket. To do that we will need to install the Python package, ``awscli``:\r\n\r\n``pip install awscli``\r\n\r\n3. Now from the top level of the project repo, we will change directory into the\r\n\"S3\" folder and run our upload command:\r\n\r\n.. code-block:: bash\r\n\r\n    cd S3\r\n    aws s3 cp . s3://example-bucket/ --recursive\r\n\r\n\r\nJust be sure to replace `example-bucket` with the name of your S3 bucket.\r\n\r\nNote: Since your AWS environment variables are exported, you should be able to\r\nestablish a connection to your S3 bucket through the AWS CLI. It should be noted,\r\nhowever, that if you find yourself encountering an error, you may need to sync your\r\nsystem's clock to match the current time. If you still experience difficulty, you\r\nmay need to export another environment variable, ``AWS_DEFAULT_REGION``, which should\r\nstore the same region as your S3 bucket (for me, that is \"us-east-1\").\r\n\r\nAfter all this work, you are *almost* ready to launch the blog (I promise the next parts\r\nare easy ;D).\r\n\r\n\r\nSetting the Timezone\r\n--------------------\r\n\r\nWhen DateTimes are created for objects, they will be created relative to your timezone.\r\nExport your timezone to the following environment variable:\r\n\r\n``BLOG_TIME_ZONE``\r\n\r\nTimezone value examples are CET, EST, and GMT, or 'Europe/Berlin', or even 'Etc/GMT+1'.\r\nTimezone values can be found in `this list \u003chttps://en.wikipedia.org/wiki/List_of_tz_database_time_zones\u003e`_.\r\n\r\n\r\nSetting Debug\r\n-------------\r\n\r\nDebugging is nifty, but must be turned off in production. The debug settings are set\r\nthrough the following environment variables:\r\n\r\n``BLOG_DEBUG`` and ``BLOG_DEBUG_PROPAGATE_EXCEPTIONS``\r\n\r\nBoth of these variables must be exported with a value of either 0 (for false) or 1\r\n(for true). I use numbers instead of False and True to be consistent with the way\r\nHeroku lists other boolean environment variables. As the name suggests, the\r\nDEBUG_PROPAGATE_EXCEPTIONS variable will show the debug exceptions even when debug\r\nmode is not enabled. This can be useful when your server is in production and you\r\nencounter HTTP 500s, where the log of stdout from the application should show the\r\ndetailed exceptions.\r\n\r\nExporting your Django Secret Key\r\n---------------------------------\r\n\r\nGenerate a Django secret key for yourself. I like to use ( https://djecrety.ir/ ).\r\nExport it to the following environment variable:\r\n\r\n``BLOG_SECRET_KEY``\r\n\r\nI recommend wrapping the key in double quotes on Linux.\r\n\r\n\r\nPopulating the Database and Creating your Initial Admin Account\r\n---------------------------------------------------------------\r\n\r\nMigrate the Database\r\n********************\r\n\r\nTo migrate the database, open a terminal to the main project folder of this repo where\r\nit is cloned and run the following commands:\r\n\r\n.. code:: bash\r\n\r\n    python manage.py makemigrations admin_pages --skip-checks\r\n    python manage.py makemigrations mail_subscription --skip-checks\r\n    python manage.py migrate --skip-checks\r\n\r\n\r\nRun the Initial Setup Script\r\n****************************\r\n\r\nNow open your interactivate project shell. If you are not already using\r\nyour virtualenv, activate it now: ``.\\venv\\Scripts\\activate`` (or ``source venv/bin/activate``).\r\nThen enter:\r\n\r\n``python manage.py shell``\r\n\r\nOnce in your shell enter the following line:\r\n\r\n.. code:: python\r\n\r\n    exec(open('initial_setup.py').read())\r\n\r\n\r\nFollow the prompts to complete initial setup. The username and password you\r\ngenerate will be what you use to log into the blog app from the login page.\r\n\r\nAfter the setup script has been run, you will have to exit the shell using ``exit()``.\r\n\r\n\r\nRunning the server, Logging in\r\n------------------------------\r\n\r\n1. The server can be started by entering ``python manage.py runserver`` from the root project\r\nfolder.\r\n\r\n2. Visit the URL (http://127.0.0.1:8000). To login, scroll down to the footer and click the\r\ncopyright symbol which links to your staff login page (\u003csite_url\u003e/account/login). Enter your\r\nadmin username and password.\r\n\r\n.. image:: ./readme_images/localserver-1-footer-login-link.png\r\n    :width: 500\r\n    :alt: Zoomed in view of footer copyright.\r\n\r\n\r\n3. After you have entered your username and password (correctly), you will be asked to configure\r\ntwo-factor authentication. I recommend using the Google Authenticator app. Follow the prompts; the\r\nprocess is straightforward. Pause on the page with the header **2FA Setup Complete**.\r\n\r\n.. image:: ./readme_images/localserver-2-2fa-complete.png\r\n    :width: 700\r\n    :alt: 2FA setup complete view\r\n\r\n\r\n4. On the **2FA Setup Complete** screen, click **Account Security Options** \u003e **Show Tokens** \u003e **Generate Tokens**.\r\nSelect over all of the tokens with your cursor and copy them. Then paste them into a text file and store them\r\nsomewhere safe. That way, if you ever are not able to use your authenticator app, you can log in with one of the\r\nbackup tokens and then reset your 2FA after you are logged in (so you can reconfigure your 2FA). This is preferable\r\nover getting locked out and having to go in through the command line to either remove your default 2FA method or to\r\ncreate a new admin account.\r\n\r\n.. image:: ./readme_images/localserver-3-2fa-backup-tokens.png\r\n    :width: 700\r\n    :alt: 2FA Backup Tokens view\r\n\r\n\r\nConfigure Your Email Recipient and Email Templates\r\n--------------------------------------------------\r\n\r\nEmail Recipient\r\n***************\r\n\r\n1. From the administrative menu, click on **Email** under **Accounts**.\r\n\r\n.. image:: ./readme_images/localserver-4-accounts-email.png\r\n    :width: 500\r\n    :alt: Administrative menu with Email option visible\r\n\r\n\r\n2. Enter the email address to which form data will be sent.\r\n\r\n.. image:: ./readme_images/localserver-5-recip-email.png\r\n    :width: 700\r\n    :alt: Edit recipient email address screen\r\n\r\n\r\nEmail Templates\r\n***************\r\n\r\nFor each form that you plan to use, you need to designate an email template for the form's data. Email templates\r\nare created in Sendinblue where each template is given an integer ID.\r\n\r\nAs an example, let's say you have the contribute page set as visible (page visibility is set in the administrative\r\nmenu by going to **Pages** \u003e **Site Look** and checking **Show Contribute page**). As such, the contribute form\r\nis active on your site. If a user submits their form data, there is currently no valid template ID that my\r\n``form_sender`` module can use to send the form data to your recipient email account. Rather, on their submission,\r\na form failure will be stored in the administrative page **Failures \u003e Mail Send Failures** and the exception listed\r\nwill say \"Invalid template id\":\r\n\r\n.. image:: ./readme_images/localserver-6-form-exception.png\r\n    :width: 400\r\n    :alt: View of logged form exception\r\n\r\n\r\nIf I look in my Sendinblue Templates, I can see that there is no form with an ID of 0 (the default my app set) and\r\nthat the correct ID, in my case, would be the template with an ID of 1 as you can see below (that is the template\r\nI have created to send the Contrib form data):\r\n\r\n.. image:: ./readme_images/localserver-7-email-template-ids.png\r\n    :width: 800\r\n    :alt: Sendinblue templates page\r\n\r\n\r\nThat template ID can be set from the admin menu from **Forms** \u003e **Contrib Form**. But that requires you to have\r\nemail templates set up! Let's get started on setting those up.\r\n\r\n\r\nCreate your first template and tell the site to use it\r\n======================================================\r\n\r\nI have created shareable links to my templates which will correctly serve the form data. Make sure you are logged\r\ninto your Sendinblue account in your web browser and follow this link, which is the template for Contrib data:\r\n\r\nhttps://my.sendinblue.com/template/kT_c4V82kD8zfJ2N6KR6jrew_aaWbgcpM.6w1HOLuABt5YY6Mwiwcjwt\r\n\r\n\r\n.. image:: ./readme_images/sendinblue-2-import-template.png\r\n    :width: 800\r\n    :alt: Sendinblue template Import screen\r\n\r\n\r\n1. You will probably be brought to an editor screen. and this is where you would want to change out my header\r\nimage with a header image of your own (or just delete the image for now). Notice that some of the text is highlighted.\r\nThat is where I have typed the parameters. For example, the actual text I typed for the title is `{{ params.title }}`,\r\n\"name\" actually has the text, `{{ params.name }}`. The \"params\" are actually received by Sendinblue from my `form_sender`\r\nmodule and it populates the template with the parameters sent to it so the recipient gets all the form data. The template\r\nformat does not matter, but if you delete a parameter, you will not receive that part of the form data which my application\r\nsends.\r\n\r\n.. image:: ./readme_images/sendinblue-3-template-highlighted-params.png\r\n    :width: 500\r\n    :alt: Sendinblue email template with params highlighted\r\n\r\n\r\nWhen you have edited the template how you want, click **Continue** \u003e **Save \u0026 Activate** \u003e **Save \u0026 Quit**\r\n(left of Save \u0026 Activate).\r\n\r\n2. Now you will see that you have one template, its title indicates that it is for sending data from the Contrib form, and\r\nits ID is 1:\r\n\r\n.. image:: ./readme_images/sendinblue-3-template-contrib-1.png\r\n    :width: 800\r\n    :alt: Sendinblue page with Contrib form ID visible\r\n\r\n\r\n3. Now to set the ID of 1 for our Contrib form, from the administrative menu of the locally hosted blog site, go to\r\n**Forms** \u003e **Contrib Form**. Change the ID from 0 to 1.\r\n\r\n.. image:: ./readme_images/localserver-9-contrib-form-manage-id.png\r\n    :width: 800\r\n    :alt: Setting the contrib form ID\r\n\r\n\r\nTesting the Contrib form\r\n========================\r\n\r\n1. Now on the public-facing Contribute page (\u003csite_url\u003e/contrib/), fill out and submit the form.\r\n\r\n.. image:: ./readme_images/localserver-10-enter-contrib-form.png\r\n    :width: 700\r\n    :alt: Filling out the contrib form\r\n\r\n\r\n2. Check the inbox of the account that you set as your email recipient on the blog. Hopefully\r\nyou will receive an email that looks similar to this one shortly:\r\n\r\n.. image:: ./readme_images/inbox-1-received-contrib-data.png\r\n    :width: 600\r\n    :alt: Received email\r\n\r\n\r\nNote: If the sender is showing up as \"Ryan's Reflections\", you will want to edit your template. From the **Templates**\r\nscreen on Sendinblue, click **Edit** on your template, and then select **Setup** and change the setup information to\r\nwhat you want:\r\n\r\n.. image:: ./readme_images/sendinblue-4-edit-setup.png\r\n    :width: 800\r\n    :alt: Edit Sendinblue setup\r\n\r\n\r\nShared Form \u0026 Newsletter Email Templates\r\n========================================\r\n\r\nLinked below are standard templates I have created for each of the forms and for newsletters. You will notice\r\nthere are 3 different Newsletter templates: one if both Featured and Recent posts are shown and a template\r\neach for exclusively for Featured or Recent posts. Those three template IDs can be set from the admin menu\r\nin **Forms** \u003e **Subscribe Form**. The subscribe form settings also ask for a \"Subscribed\" template ID. That\r\nis the template I used to send an email to a user to confirm that they have subscribed.\r\n\r\nI recommend importing each of these templates to your Sendinblue since they already include all the parameters\r\nthat my application uses. You can always re-style them to your preferences.\r\n\r\n- **Contrib**: https://my.sendinblue.com/template/PAZS713LD72mv1dYrWwbqHfenkYN1reKaZXHIkwpuoBJCNIs2MLiou7\\_\r\n- **Contact**: https://my.sendinblue.com/template/mzdxFvS9RjEq9CjRTnnUg7oTFAWYIyNAmBaycpDJN5hSJJnzYQqd.VOd\r\n- **Email Contact**: https://my.sendinblue.com/template/LrB2yp2rOgsukWL6gNBexT1WTDVOnt1uHstJzsW.2XPBATPL8fZGequ1\r\n\r\n- **Subscribed**: https://my.sendinblue.com/template/QtIjQNca3qR.qxRCYYvTYQHR.M50VbjIj7MSMQMFtjJS.0wRE89ujK9P\r\n- **Featured and Recent Newsletter**: https://my.sendinblue.com/template/aVouJ3Bqr9Jv0fJChWAUy1TBexSx7uk7S8nFJleCwFPYfMh1TnpVLohP\r\n- **Featured Only Newsletter**: https://my.sendinblue.com/template/AibNTaHWXCwXhNrQqoDuq9N9vpNtEauMTgDzk33y.wu7OxqlRr7FcHWV\r\n- **Recent Only Newsletter**: https://my.sendinblue.com/template/o6hO1AVEomgL6vnrRPrOB3NZeFKri4KLg72loFo7tHE6m28BMGd0.slY\r\n\r\nAfter you have imported the templates, update the respective template IDs in the **Forms** settings in the admin\r\nmenu as I did in the example above with the Contrib form.\r\n\r\nCustomizations\r\n--------------\r\n\r\nThe layout is already designed to be fully responsive to screen size and the layout also responds to the content\r\nyou enter. For example, on the about page, the layout will display either centered, if you have one main about section\r\nor it will display two side-by-side text boxes if you have two sections of text. Similarly, the contact page and the footer\r\nwill format appropriately according to what information you enter. Each page has a variety of settings through the\r\nadministrative menu in the **Pages** section.\r\n\r\nThe Style\r\n**********\r\n\r\nAll the style rules are in the `S3` folder (although they get uploaded to your S3 storage bucket). You can edit\r\nfiles stored in your S3 bucket directly in Visual Studio Code using the AWS Toolkit extension. Assuming you have\r\nset the AWS environment variables and your system clock matches the correct time for your timezone, AWS Toolkit should\r\nautomatically allow you to access your cloud services and your S3 bucket will be found under **S3** in the AWS Toolkit\r\nExplorer pane.\r\n\r\nOne basic style modification that I recommend is changing the green accent color that is used throughout the blog. This\r\ncolor is set in the root class as ``--clr-brand-green`` and a slightly darker green color used for when buttons are hovered\r\nover is defined by ``--clr-brand-green-hover``. These properties are set in the main stylesheet (\u003cS3bucket\u003e/css/style.css).\r\nRather than refactor the names, which would not only require you to do so in the CSS file but in the many different HTML\r\ntemplates as well, simply change the actual color values to whatever colors you prefer. Just make sure to use the HSL format\r\n(e.g., 28, 84%, 53%). I've noticed that the degree symbol on the first number does not get parsed correctly so don't use it.\r\n\r\n.. image:: ./readme_images/style-1-accent-color.png\r\n    :width: 800\r\n    :alt: Timeline page with orange accent color\r\n\r\n\r\nAbove: What the timeline page looks like when ``--clr-brand-green`` is set to `28, 84%, 53%` in the main `style.css` file.\r\n\r\n\r\nAdministering the Site\r\n----------------------\r\n\r\nDue to my lack of endless free time, I have not yet documented the site administration. If you would like to set up this blog\r\nproject but want to know more about how to use the administrative views to manage your content and users, contact me at\r\nhaasrr@etsu.edu.\r\n\r\n\r\nDeployment\r\n----------\r\n\r\nI don't really have any special notes for deployment at this time. To my knowledge, deploying this application should\r\nnot be particularly different than the deployment of any other Django project. Of course, remember to set your domain\r\n(in place of 'yoursite.com') in ``ALLOWED_HOSTS`` in ``blog/settings.py`` and all the environment variables needed to\r\nrun the blog app in development are still needed in production so you should probably export all of your environment\r\nvariables needed for it prior to trying to deploy it.\r\n\r\nBugs\r\n----\r\n\r\nIf you notice a bug, please report it to me on Github. Understand that this is one of several of my side projects\r\nand I do not plan to devote an exhorbitant amount of time toward minor bugfixes. If you want to contribute to this code,\r\ncontact me (a clean refactor would be welcome :D).\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhaasr%2Fcollaborative-blog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhaasr%2Fcollaborative-blog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhaasr%2Fcollaborative-blog/lists"}