diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..8ef8a69
Binary files /dev/null and b/.DS_Store differ
diff --git a/.env.example b/.env.example
index 8f1c625..d8e374c 100644
--- a/.env.example
+++ b/.env.example
@@ -2,7 +2,6 @@
SECRET_KEY=
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost
-DEVELOPMENT_MODE=False
# Website details
SITE_URL=SITE URL
@@ -43,6 +42,11 @@ EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=True
DEFAULT_FROM_EMAIL=
+POST_TYPE_CHOICES=article:Article,quiz:Quiz,lesson:Lesson
+SHOW_EMTPY_CATEGORIES=False
+
#ads.txt file content
-MY_ADS_TXT_CONTENT=
\ No newline at end of file
+LOAD_GOOGLE_TAG_MANAGER=True
+LOAD_GOOGLE_ADS=True
+MY_ADS_TXT_CONTENT=
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..6e1473a
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: [nilandev]
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..68606dc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,20 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[BUG]"
+labels: waiting for triage
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..19171e1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[New Feature] "
+labels: waiting for triage
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
new file mode 100644
index 0000000..f198fa8
--- /dev/null
+++ b/.github/workflows/pylint.yml
@@ -0,0 +1,24 @@
+name: Pylint
+
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install pylint
+ - name: Analysing the code with pylint
+ run: |
+ pylint -d C0114,C0115,C0116,C0301,E1101,R0903,R0901,W0613 $(git ls-files '*.py') --fail-under=9
diff --git a/.gitignore b/.gitignore
index 525bf51..088a14b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,16 +64,8 @@ docs/_build/
/bloggy_frontend/node_modules/
/bloggy/static/
/bloggy/static
-/bloggy/static/debug_toolbar
-/bloggy/static/hitcount
-/bloggy/static/media
-/bloggy/static/rest_framework
-/bloggy/static/summernote
-/bloggy/static/colorfield
-/bloggy/static/admin/
-media/uploads/default_avatar.png
-media/uploads/articles/*
-media/uploads/categories/*
-media/uploads/course/*
-media/uploads/quiz/*
-media/uploads/user/*
\ No newline at end of file
+media/uploads/*
+media/uploads/
+
+# Virtual Environments Ignore
+.venv
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..0111a6b
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,2 @@
+[MASTER]
+ignore=migrations
\ No newline at end of file
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
deleted file mode 100644
index 4b249b7..0000000
--- a/.readthedocs.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-# .readthedocs.yaml
-# Read the Docs configuration file
-# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
-
-# Required
-version: 2
-
-# Set the OS, Python version and other tools you might need
-build:
- os: ubuntu-22.04
- tools:
- python: "3.12"
- # You can also specify other tool versions:
- # nodejs: "19"
- # rust: "1.64"
- # golang: "1.19"
-
-# Build documentation in the "docs/" directory with Sphinx
-sphinx:
- configuration: docs/conf.py
-
-# Optionally build your docs in additional formats such as PDF and ePub
-# formats:
-# - pdf
-# - epub
-
-# Optional but recommended, declare the Python requirements required
-# to build your documentation
-# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
-# python:
-# install:
-# - requirements: docs/requirements.txt
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..0c87cf0
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,47 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Djanog:Run",
+ "type": "python",
+ "request": "launch",
+ "stopOnEntry": false,
+ "python": "${workspaceRoot}/venv/bin/python3",
+ "program": "${workspaceFolder}/manage.py",
+ "args": [
+ "runserver"
+ ],
+ "django": false,
+ "justMyCode": true,
+ "autoReload": {
+ "enable": true
+ }
+ },
+ {
+ "name": "Extension",
+ "type": "extensionHost",
+ "request": "launch",
+ "runtimeExecutable": "${execPath}",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}"
+ ]
+ },
+ {
+ "name": "Djanog:Debug",
+ "type": "python",
+ "request": "launch",
+ "stopOnEntry": false,
+ "python": "${workspaceRoot}/venv/bin/python3",
+ "program": "${workspaceFolder}/manage.py",
+ "args": [
+ "runserver"
+ ],
+ "django": true,
+ "justMyCode": true
+ },
+ ],
+ "compounds": []
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5d23c7f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "editor.wordWrapColumn": 120,
+ "python.analysis.typeCheckingMode": "basic",
+ "python.formatting.provider": "autopep8",
+ "editor.formatOnSave": false,
+ "python.linting.enabled": true,
+ "python.linting.lintOnSave": true,
+ // "editor.fontFamily": "Dank Mono, JetBrains Mono NL, Fira Code, Menlo, Monaco, 'Courier New', monospace",
+ // "editor.fontSize": 14,
+}
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..68179aa
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,131 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Project Maintainers are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Project Maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported (this can be done anonymously) to the Project Maintainers responsible for enforcement at https://stacktips.com/contact.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All Project Maintainers are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Project Maintainers will follow these Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from Project Maintainers, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][mozilla coc].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][faq]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[mozilla coc]: https://github.com/mozilla/diversity
+[faq]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e457f24
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,42 @@
+# Contributing Guide
+
+## Creating an Issue
+Before **creating** an Issue for `features`/`bugs`/`improvements` please follow these steps:
+1. Search existing Issues before creating a new issue (has someone raised this already)
+1. If it doesn't exist create a new issue giving as much context as possible
+1. All issues are automatically given the label `status: waiting for triage` and are automatically locked so no comments can be made
+1. If you wish to work on the Issue once it has been triaged and label changed to `status: ready for dev`, please include this in your Issue description
+
+## Working on an Issue (get it assigned to you)
+
+Before working on an existing Issue please follow these steps:
+
+1. Only ask to be assigned 1 **open** issue at a time
+1. Look out for the Issue label `status: ready for dev`
+1. Comment asking for the issue to be assigned to you.
+1. After the Issue is assigned to you, you can start working on it
+1. **Only** start working on this Issue (and open a Pull Request) when it has been assigned to you - this will prevent confusion, multiple people working on the same issue, and work not being used
+1. reference the Issue in your Pull Request (for example `closes #123`)
+
+### Notes:
+- check the `Assignees` box at the top of the page to see if the issue has been assigned to someone else before requesting this be assigned to you
+- if an Issue is unclear, ask questions to get more clarity before asking to have the Issue assigned to you
+- only request to be assigned an Issue if you know how to work on it
+- an Issue can be assigned to multiple people if you all agree to collaborate on the issue (the Pull Request can contain commits from different collaborators)
+- any Issues that have no activity after 2 weeks will be unassigned and re-assigned to someone else
+
+## Reviewing Pull Requests
+
+We welcome everyone to review Pull Requests, it is a great way to learn, network, and support each other.
+
+### DOs
+
+- be kind and respectful
+- use inline comments to explain your suggestions
+- use inline suggestions to propose changes
+
+### DON'Ts
+
+- do not be rude, disrespectful, or aggressive
+- do not repeat feedback, this creates more noise than value (check the existing conversation), use GitHub reactions if you agree/disagree with a comment
+- do not blindly approve pull requests to improve your GitHub contributor's graph
diff --git a/LICENSE b/LICENSE
index d6a9326..77b4268 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,340 +1,21 @@
-GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.) You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
-
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
-
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
-
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software interchange; or,
-
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
-
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation. If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
- NO WARRANTY
-
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- {description}
- Copyright (C) {year} {fullname}
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along
- with this program; if not, write to the Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- {signature of Ty Coon}, 1 April 1989
- Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
-
+MIT License
+
+Copyright (c) 2021 - 2023 Nilanchala Panigrahy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
diff --git a/README.md b/README.md
index ab3913a..e72a29c 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,44 @@
# Bloggy
-Introducing Bloggy! The Open-Source Blogging Platform is Built on Python and Django framework that powers stacktips.com blog.
+Introducing Bloggy! The Open-Source Blogging Platform for developers. It is Built on Python and Django framework and
+powers [stacktips.com](https://stacktips.com) blog.
+
+If you are a new contributor to this project, have a look out for issues that have
+the [Hacktoberfest](https://github.com/StackTipsLab/Bloggy/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest) label.
## Key Features
-Along with tons of features aimed at enhancing the development and blogging experience.
+
+Along with tons of features aimed at enhancing the development and blogging experience.
+
* **Signup and Login**: Seamlessly create your account and log in to access exclusive content.
* **Magic Link Sign-In**: Forget passwords! We've streamlined the login process with magic link sign-in.
-* **Create and Publish**: Share your knowledge with the world by creating and publishing articles, courses, and quizzes effortlessly.
-* **Customized Admin Dashboard**: Manage your content efficiently with a user-friendly admin dashboard designed with you in mind.
+* **Create and Publish**: Share your knowledge with the world by creating and publishing articles, courses, and quizzes
+ effortlessly.
+* **Customized Admin Dashboard**: Manage your content efficiently with a user-friendly admin dashboard designed with you
+ in mind.
* **Sitemaps**: Enhance discoverability with built-in sitemaps that improve search engine ranking.
-* **Webmaster Notifications**: Get noticed! StackTips automates Google and Bing webmaster notifications to ensure your content reaches a wider audience.
+* **Webmaster Notifications**: Get noticed! StackTips automates Google and Bing webmaster notifications to ensure your
+ content reaches a wider audience.

-
## Get Involved!
-Are you a developer looking to enhance your skills, share your knowledge, or simply be curious about the inner workings of a developer-centric blog platform? Now's your chance!
-Want to contribute right away?
+Are you a developer looking to enhance your skills, share your knowledge, or simply be curious about the inner workings
+of a developer-centric blog platform? Now's your chance!
+
+Want to contribute right away?
Check out the issues section. https://github.com/StackTipsLab/bloggy/issues
## Installation Guide
+
Checkout the code from our git repository
git clone git@github.com:StackTipsLab/bloggy.git
Create a virtual env
-
+
```shell
python3 -m venv .venv
source .venv/bin/activate
@@ -39,12 +50,13 @@ Install python dependencies
pip3 install -r requirements.txt
```
-Rename the `.env.example` file to `.env` and provide all the configuration details including Database, and Email Configurations. Bare minimum, you will need these properties to get started.
+Rename the `.env.example` file to `.env` and provide all the configuration details including Database, and Email
+Configurations. Bare minimum, you will need these properties to get started.
+
```properties
SECRET_KEY=
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost
-DEVELOPMENT_MODE=True
# Your database configuration details
DB_NAME=bloggy
@@ -67,7 +79,6 @@ Create superuser
python3 manage.py createsuperuser
```
-
Collect static files before publishing or development.
```shell
@@ -80,7 +91,6 @@ Start the application
python3 manage.py runserver
```
-
## Bloggy Frontend Module
For building frontend code, you will need the following node version.
@@ -99,9 +109,21 @@ Once you have the above node version installed, install node dependencies using
npm install
```
-Now, you can build
+Now, you can build
```shell
npm run start
npm run build # to generate a production build
```
+
+## Importing Demo Content
+
+We currently supports importing the categories from CSV file. This can be done using the `runseed` command. All you need
+to do is to provide the base path where your `.csv` files are located.
+
+The sample CSV files are located in `bloggy/demo_content` directory.
+
+```shell
+python3 manage.py runseed --dir=demo_content
+```
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..1fc9e96
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,15 @@
+# Reporting Security Issues
+
+The StackTips community take security bugs in seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
+
+To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/StackTipsLab/bloggy/security/advisories/new) tab.
+
+We will review the issue and take the necessary steps in handling your report. During the issue investigation we may ask for additional information or guidance.
+
+## Third party security issues
+Report security bugs in third-party modules to the person or team maintaining the module.
+
+
+If you find security issues in node module, you can report them directly through the [npm contact form](https://www.npmjs.com/support)
+
+To report a vulnerability in Django, you [visit this link here](https://docs.djangoproject.com/en/dev/internals/security/#reporting-security-issues)
\ No newline at end of file
diff --git a/bloggy/.DS_Store b/bloggy/.DS_Store
index a580bdf..8e1f43b 100644
Binary files a/bloggy/.DS_Store and b/bloggy/.DS_Store differ
diff --git a/bloggy/admin/__init__.py b/bloggy/admin/__init__.py
index 3fbbd2b..118d098 100644
--- a/bloggy/admin/__init__.py
+++ b/bloggy/admin/__init__.py
@@ -1,7 +1,9 @@
-from .misc_admin import *
-from .article_admin import *
+from .admin import *
+from .post_admin import *
from .category_admin import *
from .course_admin import *
-from .quiz_admin import *
-from .user_admin import *
+from .misc_admin import *
from .subscriber_admin import *
+from .user_admin import *
+from .page_admin import *
+from .quiz_admin import *
diff --git a/bloggy/admin/admin.py b/bloggy/admin/admin.py
new file mode 100644
index 0000000..5a81ae8
--- /dev/null
+++ b/bloggy/admin/admin.py
@@ -0,0 +1,62 @@
+from django import forms
+from django_summernote.admin import SummernoteModelAdmin
+
+seo_fieldsets = ('SEO Settings', {
+ 'fields': ('meta_title', 'meta_description', 'meta_keywords'),
+})
+
+publication_fieldsets = ('Publication options', {
+ 'fields': ('publish_status', 'published_date',),
+})
+
+
+def publish(model_admin, request, queryset):
+ queryset.update(publish_status='LIVE')
+
+
+publish.short_description = "Publish"
+
+
+def unpublish(model_admin, request, queryset):
+ queryset.update(publish_status='DRAFT')
+
+
+unpublish.short_description = "Unpublish"
+
+
+class BloggyAdminForm(forms.ModelForm):
+ excerpt = forms.CharField(widget=forms.Textarea(attrs={'rows': 3, 'cols': 105}))
+ title = forms.CharField(widget=forms.TextInput(attrs={'size': 105}))
+ meta_title = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 1, 'cols': 100}))
+ meta_description = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
+ meta_keywords = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
+
+ class Meta:
+ abstract = True
+
+
+class BloggyAdmin(SummernoteModelAdmin):
+ actions = [publish, unpublish]
+ list_per_page = 50
+
+ def published_date_display(self, obj):
+ if obj.published_date:
+ return obj.published_date.strftime("%b %d, %Y")
+ return "-"
+
+ published_date_display.short_description = "Date Published"
+
+ def author_display(self, obj):
+ return '' + obj.author.name
+
+ author_display.short_description = "Author"
+
+ def is_published(self, queryset):
+ if queryset.publish_status == 'LIVE':
+ return True
+ return False
+
+ is_published.boolean = True
+
+ class Meta:
+ abstract = True
diff --git a/bloggy/admin/article_admin.py b/bloggy/admin/article_admin.py
deleted file mode 100644
index 07bd013..0000000
--- a/bloggy/admin/article_admin.py
+++ /dev/null
@@ -1,174 +0,0 @@
-from django import forms
-from django.contrib import admin
-from django.contrib.auth import get_user_model
-from django.db.models import TextField
-from django.forms import Textarea, BaseInlineFormSet
-from django.urls import reverse
-from django.utils import timezone
-from django.utils.html import format_html
-from django.utils.safestring import mark_safe
-from django_summernote.admin import SummernoteModelAdmin
-
-from bloggy.admin.misc_admin import publish, unpublish
-from bloggy.models import Article, PostMeta
-
-
-class ArticleForm(forms.ModelForm):
- excerpt = forms.CharField(widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
- title = forms.CharField(widget=forms.TextInput(attrs={'size': 105}))
- model = Article
- keywords = forms.CharField(widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
-
-
-class PostMetaInlineFormSet(BaseInlineFormSet):
- def __init__(self, *args, **kwargs):
- self.initial = [
- {'meta_key': 'template_type', 'meta_value': "standard"},
- {'meta_key': 'seo_title', 'meta_value': ""},
- {'meta_key': 'seo_description', 'meta_value': ""},
- {'meta_key': 'seo_keywords', 'meta_value': ""},
- ]
- super(PostMetaInlineFormSet, self).__init__(*args, **kwargs)
-
-
-class PostMetaInline(admin.TabularInline):
- model = PostMeta
- extra = 0
- formfield_overrides = {
- TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 105})},
- }
- formset = PostMetaInlineFormSet
-
-
-@admin.register(Article)
-class ArticleAdmin(SummernoteModelAdmin):
- def get_form(self, request, obj=None, **kwargs):
- form = super(ArticleAdmin, self).get_form(request, obj, **kwargs)
- form.base_fields["author"].queryset = get_user_model().objects.filter(is_staff=True)
- return form
-
- prepopulated_fields = {"slug": ("title",)}
- list_display = (
- 'id',
- 'title',
- 'category_display',
- 'post_type',
- 'template_type',
- 'is_published',
- 'author_link',
- 'display_order',
- 'published_date_display',
- 'updated_date_display'
- )
- list_filter = (
- 'publish_status',
- ('post_type', admin.ChoicesFieldListFilter),
- ('template_type', admin.ChoicesFieldListFilter),
- ("category", admin.RelatedOnlyFieldListFilter),
- ("author", admin.RelatedOnlyFieldListFilter),
- ("course", admin.RelatedOnlyFieldListFilter),
- 'is_featured',
- ("video_id", admin.BooleanFieldListFilter),
- )
-
- fieldsets = (
- (None, {
- 'fields': ('title', 'excerpt', 'keywords', 'slug', 'content', 'thumbnail', 'author', 'category',)
- }),
- ('Publication options', {
- 'fields': ('publish_status', 'published_date',),
- }),
- ('Advanced options', {
- 'fields': ('post_type', 'template_type', 'course', 'duration', 'difficulty', 'video_id', 'is_featured',
- 'display_order'),
- }),
- )
-
- search_fields = ['title']
- summernote_fields = ('content',)
- readonly_fields = ['updated_date', 'created_date']
- date_hierarchy = 'published_date'
- form = ArticleForm
- ordering = ('-created_date',)
- list_display_links = ['title']
- list_per_page = 50
- actions = [publish, unpublish]
- inlines = [PostMetaInline]
-
- def published_date_display(self, obj):
- return format_html(
- '{}'.format(obj.published_date.strftime("%m/%d/%Y") if obj.published_date else "-"))
-
- published_date_display.short_description = "Published on"
-
- def has_video(self, obj):
- return self.videoId
-
- def updated_date_display(self, obj):
- return format_html(
- '{}'.format(obj.updated_date.strftime("%m/%d/%Y") if obj.published_date else "-"))
-
- updated_date_display.short_description = "Updated on"
-
- def category_display(self, obj):
- tags = "".join([
- "" + cat.title + "" for cat in obj.category.all()
- ])
- return format_html(tags)
-
- category_display.short_description = "Categories"
-
- def author_link(self, obj):
- url = reverse("admin:bloggy_myuser_change", args=[obj.author.id])
- if obj.author.name:
- link = '%s' % (url, obj.author.name)
- else:
- link = '%s' % (url, obj.author.username)
- return mark_safe(link)
-
- author_link.short_description = 'Author'
-
- def view_on_site(self, obj):
- url = reverse('article_single', kwargs={'slug': obj.slug})
- return url + "?context=preview"
-
- def save_model(self, request, obj, form, change):
- if "publish_status" in form.changed_data and obj.publish_status == "LIVE" and not obj.published_date:
- obj.published_date = timezone.now()
- if not obj.pk:
- obj.author = request.user
-
- super().save_model(request, obj, form, change)
-
- def live_category(self, queryset):
- if queryset.publish_status == 'LIVE':
- return True
- return False
-
- def is_published(self, queryset):
- if queryset.publish_status == 'LIVE':
- return True
- return False
-
- is_published.boolean = True
- is_published.short_description = "Status"
-
- def has_excerpt(self, queryset):
- if queryset.excerpt is None:
- return False
- return True
-
- has_excerpt.boolean = True
-
-
-@admin.register(PostMeta)
-class PostMetaAdmin(admin.ModelAdmin):
- list_display = ['id', 'article_link', 'meta_key', 'meta_value']
- list_filter = ['meta_key']
-
- def article_link(self, obj):
- url = reverse("admin:bloggy_article_change", args=[obj.article.id])
- link = '%s' % (url, obj.article.title)
- return mark_safe(link)
-
- article_link.short_description = 'Author'
diff --git a/bloggy/admin/category_admin.py b/bloggy/admin/category_admin.py
index 84c8745..b1af394 100644
--- a/bloggy/admin/category_admin.py
+++ b/bloggy/admin/category_admin.py
@@ -2,14 +2,18 @@
from django.contrib import admin
from django.utils.html import format_html
+from bloggy.admin import seo_fieldsets
+from bloggy.admin.admin import publish, unpublish
from bloggy.models import Category
-from bloggy.admin.misc_admin import publish, unpublish
class CategoryForm(forms.ModelForm):
- description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3, 'cols': 105}))
+ excerpt = forms.CharField(widget=forms.Textarea(attrs={'rows': 3, 'cols': 105}))
title = forms.CharField(widget=forms.TextInput(attrs={'size': 105}))
slug = forms.CharField(widget=forms.TextInput(attrs={'size': 105}))
+ meta_title = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 1, 'cols': 100}))
+ meta_description = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
+ meta_keywords = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
model = Category
@@ -18,16 +22,24 @@ class CategoryAdmin(admin.ModelAdmin):
list_display = (
'title',
'id',
- 'is_published',
- 'logo_tag',
+ 'is_category_published',
+ 'thumbnail_tag',
'article_count',
'slug',
'display_updated_date',
'display_color',
)
+
+ fieldsets = (
+ (None, {
+ 'fields': ('title', 'slug', 'excerpt', 'thumbnail', 'color', 'publish_status')
+ }),
+ seo_fieldsets
+ )
+
show_change_link = True
form = CategoryForm
- readonly_fields = ['logo_tag', 'article_count']
+ readonly_fields = ['thumbnail_tag', 'article_count']
prepopulated_fields = {"slug": ("title",)}
list_per_page = 50
ordering = ('-article_count',)
@@ -35,12 +47,12 @@ class CategoryAdmin(admin.ModelAdmin):
search_fields = ['title', 'slug']
actions = [publish, unpublish]
- def is_published(self, queryset):
+ def is_category_published(self, queryset):
if queryset.publish_status == 'LIVE':
return True
return False
- is_published.boolean = True
+ is_category_published.boolean = True
def display_updated_date(self, obj):
return obj.updated_date.strftime("%m/%d/%Y") if obj.updated_date else "-"
@@ -48,6 +60,6 @@ def display_updated_date(self, obj):
display_updated_date.short_description = "Published on"
def display_color(self, obj):
- return format_html('
'.format(obj.color))
+ return format_html(f'')
display_color.short_description = "Color"
diff --git a/bloggy/admin/comment_admin.py b/bloggy/admin/comment_admin.py
index 016213a..1711552 100644
--- a/bloggy/admin/comment_admin.py
+++ b/bloggy/admin/comment_admin.py
@@ -1,6 +1,6 @@
+from django.contrib import admin
from django_summernote.admin import SummernoteModelAdmin
-from django.contrib import admin
from bloggy.models.comment import Comment
@@ -21,4 +21,4 @@ class CommentAdmin(SummernoteModelAdmin):
'active')
list_filter = ('active', 'comment_date')
search_fields = ('user', 'user', 'comment_content')
- actions = ['approve_comments']
\ No newline at end of file
+ actions = ['approve_comments']
diff --git a/bloggy/admin/course_admin.py b/bloggy/admin/course_admin.py
index 2e6bd9f..138c5b0 100644
--- a/bloggy/admin/course_admin.py
+++ b/bloggy/admin/course_admin.py
@@ -1,46 +1,49 @@
-from django import forms
from django.contrib import admin
-from django_summernote.admin import SummernoteModelAdmin
+from bloggy.admin import BloggyAdmin, BloggyAdminForm, seo_fieldsets, publication_fieldsets
from bloggy.models.course import Course
-from bloggy.admin.misc_admin import publish, unpublish
-class CourseForm(forms.ModelForm):
- excerpt = forms.CharField(
- widget=forms.Textarea(attrs={'rows': 3, 'cols': 105}))
- title = forms.CharField(widget=forms.TextInput(attrs={'size': 105}))
+class CourseForm(BloggyAdminForm):
model = Course
@admin.register(Course)
-class CourseAdmin(SummernoteModelAdmin):
- prepopulated_fields = {"slug": ("title",)}
- list_display = ('id', 'title', 'is_published', 'thumbnail_tag',
- 'display_order', 'author_display', 'published_date_display', 'display_order')
- list_filter = ('difficulty',
- ("category", admin.RelatedOnlyFieldListFilter),
- )
+class CourseAdmin(BloggyAdmin):
+ prepopulated_fields = {
+ "slug": ("title",)
+ }
+ list_display = (
+ 'id',
+ 'title',
+ 'is_published',
+ 'thumbnail_tag',
+ 'display_order',
+ 'author_display',
+ 'published_date_display',
+ 'display_order')
+
+ list_filter = (
+ 'difficulty',
+ ("category", admin.RelatedOnlyFieldListFilter),
+ )
+
+ fieldsets = ((None, {'fields': (
+ 'title',
+ 'excerpt',
+ 'slug',
+ 'description',
+ 'display_order',
+ 'thumbnail',
+ 'category',
+ 'difficulty',
+ 'is_featured')
+ }), publication_fieldsets, seo_fieldsets)
+
summernote_fields = ('description',)
readonly_fields = ['thumbnail_tag']
ordering = ('-display_order',)
list_display_links = ['title']
-
- def published_date_display(self, obj):
- return obj.published_date.strftime("%b %d, %Y")
-
- published_date_display.short_description = "Date Published"
-
- def author_display(self, obj):
- return '' + obj.author.name
-
- author_display.short_description = "Author"
-
form = CourseForm
- actions = [publish, unpublish]
- def is_published(self, queryset):
- if queryset.publish_status == 'LIVE':
- return True
- return False
- is_published.boolean = True
+
diff --git a/bloggy/admin/misc_admin.py b/bloggy/admin/misc_admin.py
index 9a0b1bf..88f58d7 100644
--- a/bloggy/admin/misc_admin.py
+++ b/bloggy/admin/misc_admin.py
@@ -1,21 +1,20 @@
from django.contrib import admin
+
import bloggy.models.option
+from bloggy import settings
+from bloggy.models import RedirectRule
-admin.site.site_header = "STACKTIPS"
-admin.site.site_title = "StackTips"
+admin.site.site_header = settings.SITE_TITLE.upper()
+admin.site.site_title = settings.SITE_TITLE
admin.site.index_title = "Dashboard"
admin.site.register(bloggy.models.option.Option)
-def publish(modelAdmin, request, queryset):
- queryset.update(publish_status='LIVE')
-
-
-publish.short_description = "Publish"
-
-
-def unpublish(modelAdmin, request, queryset):
- queryset.update(publish_status='DRAFT')
-
-
-unpublish.short_description = "Unpublish"
+@admin.register(RedirectRule)
+class RedirectRuleAdmin(admin.ModelAdmin):
+ list_display = (
+ 'source',
+ 'destination',
+ 'status_code',
+ 'note',
+ )
diff --git a/bloggy/admin/page_admin.py b/bloggy/admin/page_admin.py
new file mode 100644
index 0000000..63ad13d
--- /dev/null
+++ b/bloggy/admin/page_admin.py
@@ -0,0 +1,36 @@
+from django.contrib import admin
+
+from bloggy.admin import BloggyAdminForm, BloggyAdmin, publication_fieldsets, seo_fieldsets
+from bloggy.models.page import Page
+
+
+class PageForm(BloggyAdminForm):
+ model = Page
+
+
+@admin.register(Page)
+class PageAdmin(BloggyAdmin):
+ prepopulated_fields = {"url": ("title",)}
+ list_display = (
+ 'id',
+ 'title',
+ 'url',
+ 'excerpt',
+ 'publish_status',
+ )
+
+ fieldsets = (
+ (None, {
+ 'fields': ('title', 'excerpt', 'url', 'content',)
+ }), publication_fieldsets, seo_fieldsets)
+
+ search_fields = ['title']
+ summernote_fields = ('content',)
+ readonly_fields = ['updated_date', 'created_date']
+ date_hierarchy = 'published_date'
+ form = PageForm
+ ordering = ('-created_date',)
+ list_display_links = ['title']
+
+ def get_form(self, request, obj=None, change=False, **kwargs):
+ return super().get_form(request, obj, change, **kwargs)
diff --git a/bloggy/admin/post_admin.py b/bloggy/admin/post_admin.py
new file mode 100644
index 0000000..ac04af1
--- /dev/null
+++ b/bloggy/admin/post_admin.py
@@ -0,0 +1,117 @@
+from django.contrib import admin
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+
+from bloggy.admin import BloggyAdmin, BloggyAdminForm, publication_fieldsets, seo_fieldsets
+from bloggy.models import Post
+
+
+class PostForm(BloggyAdminForm):
+ model = Post
+
+
+@admin.register(Post)
+class PostAdmin(BloggyAdmin):
+ prepopulated_fields = {"slug": ("title",)}
+ search_fields = ['title']
+ summernote_fields = ('content',)
+ readonly_fields = ['updated_date', 'created_date']
+ date_hierarchy = 'published_date'
+ form = PostForm
+ ordering = ('-created_date',)
+ list_display_links = ['title']
+ list_display = (
+ 'id',
+ 'title',
+ 'category_display',
+ 'post_type',
+ 'template_type',
+ 'is_published',
+ 'author_link',
+ 'display_order',
+ 'published_date_display',
+ 'updated_date_display'
+ )
+ list_filter = (
+ 'publish_status',
+ ('post_type', admin.ChoicesFieldListFilter),
+ ('template_type', admin.ChoicesFieldListFilter),
+ ("category", admin.RelatedOnlyFieldListFilter),
+ ("author", admin.RelatedOnlyFieldListFilter),
+ ("course", admin.RelatedOnlyFieldListFilter),
+ 'is_featured',
+ ("video_id", admin.BooleanFieldListFilter),
+ )
+
+ fieldsets = (
+ (None, {
+ 'fields': ('title', 'excerpt', 'slug', 'content', 'thumbnail', 'author', 'category',)
+ }),
+ publication_fieldsets,
+ ('Advanced options', {
+ 'fields': ('post_type', 'template_type', 'course', 'difficulty', 'video_id', 'github_link', 'is_featured',
+ 'display_order'),
+ }),
+ seo_fieldsets)
+
+ def get_changeform_initial_data(self, request):
+ return {
+ 'meta_title': '{title}',
+ 'meta_description': '{excerpt}'
+ }
+
+ def get_form(self, request, obj=None, change=False, **kwargs):
+ form = super().get_form(request, obj, change, **kwargs)
+ form.base_fields["author"].queryset = get_user_model().objects.filter(is_staff=True)
+ return form
+
+ def updated_date_display(self, obj):
+ return format_html(
+ f'{obj.updated_date.strftime("%m/%d/%Y") if obj.published_date else "-"}')
+
+ updated_date_display.short_description = "Updated on"
+
+ def category_display(self, obj):
+ tags = "".join([
+ "" + cat.title + "" for cat in obj.category.all()
+ ])
+ return format_html(tags)
+
+ category_display.short_description = "Categories"
+
+ def author_link(self, obj):
+ url = reverse("admin:bloggy_user_change", args=[obj.author.id])
+ if obj.author.name:
+ link = f'{obj.author.name}'
+ else:
+ link = f'{obj.author.username}'
+ return mark_safe(link)
+
+ author_link.short_description = 'Author'
+
+ def view_on_site(self, obj):
+ url = reverse('post_single', kwargs={'slug': obj.slug})
+ return url + "?context=preview"
+
+ def save_model(self, request, obj, form, change):
+ if "publish_status" in form.changed_data and obj.publish_status == "LIVE" and not obj.published_date:
+ obj.published_date = timezone.now()
+ if not obj.pk:
+ obj.author = request.user
+
+ super().save_model(request, obj, form, change)
+
+ def live_category(self, queryset):
+ if queryset.publish_status == 'LIVE':
+ return True
+ return False
+
+ def has_excerpt(self, queryset):
+ if queryset.excerpt is None:
+ return False
+ return True
+
+ has_excerpt.boolean = True
diff --git a/bloggy/admin/quiz_admin.py b/bloggy/admin/quiz_admin.py
index 236686f..17def79 100644
--- a/bloggy/admin/quiz_admin.py
+++ b/bloggy/admin/quiz_admin.py
@@ -1,7 +1,54 @@
from django import forms
from django.contrib import admin
-from bloggy.models import Article
-from bloggy.models.quizzes import QuizAnswer, QuizQuestion, UserQuizScore
+from django.utils.html import format_html
+
+from bloggy.admin import BloggyAdminForm, BloggyAdmin, publication_fieldsets, seo_fieldsets
+from bloggy.models.quizzes import QuizAnswer, QuizQuestion, Quiz, UserQuizScore
+
+
+class QuizForm(BloggyAdminForm):
+ model = Quiz
+
+
+@admin.register(Quiz)
+class QuizAdmin(BloggyAdmin):
+ prepopulated_fields = {
+ "slug": ("title",)
+ }
+ list_display = (
+ 'id',
+ 'title',
+ 'category',
+ 'is_published',
+ 'display_order',
+ 'published_date_display')
+
+ list_filter = (
+ 'publish_status',
+ ("category", admin.RelatedOnlyFieldListFilter),
+ )
+
+ fieldsets = (
+ (None, {
+ 'fields': (
+ 'title', 'excerpt', 'slug', 'content', 'thumbnail', 'category', 'difficulty', 'is_featured', 'duration')
+ }),
+ publication_fieldsets, seo_fieldsets)
+
+ summernote_fields = ('description',)
+ readonly_fields = ['thumbnail_tag']
+ ordering = ('-display_order',)
+ list_display_links = ['title']
+ form = QuizForm
+
+
+ def thumbnail_tag(self):
+ if self.thumbnail:
+ return format_html(f'
')
+ return ""
+
+ thumbnail_tag.short_description = 'Logo'
+ thumbnail_tag.allow_tags = True
class QuizAnswerInLine(admin.TabularInline):
@@ -18,12 +65,6 @@ class QuizQuestionForm(forms.ModelForm):
@admin.register(QuizQuestion)
class QuizQuestionAdmin(admin.ModelAdmin):
-
- def get_form(self, request, obj=None, change=False, **kwargs):
- form = super().get_form(request, obj, **kwargs)
- form.base_fields["article"].queryset = Article.objects.filter(post_type__exact="quiz")
- return form
-
inlines = [QuizAnswerInLine]
list_display = ('title', 'type')
form = QuizQuestionForm
@@ -43,7 +84,7 @@ class QuizQuestionAdmin(admin.ModelAdmin):
def display_question_id(self, obj):
return obj.question.id
- display_question_id.short_description = "Question ID"
+ display_question_id.short_description = "Question id"
def display_question_title(self, obj):
return obj.question.title
diff --git a/bloggy/admin/subscriber_admin.py b/bloggy/admin/subscriber_admin.py
index 6d9ceb0..29d46e1 100644
--- a/bloggy/admin/subscriber_admin.py
+++ b/bloggy/admin/subscriber_admin.py
@@ -1,4 +1,3 @@
-from django.contrib import admin
from django import forms
from django.contrib import admin
diff --git a/bloggy/admin/user_admin.py b/bloggy/admin/user_admin.py
index b958ca5..e6763a1 100644
--- a/bloggy/admin/user_admin.py
+++ b/bloggy/admin/user_admin.py
@@ -1,19 +1,27 @@
-from django import forms
from django.contrib import admin
-from django.utils.html import format_html
-from bloggy.models import MyUser
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
+from django.utils.html import format_html
+from bloggy.models import User
-@admin.register(MyUser)
+
+@admin.register(User)
class MyUserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = (
- 'username', 'email_display', 'full_name_display', 'is_staff', 'is_active', 'date_joined', 'last_login',
- 'profile_photo_tag', 'articles_count_display',)
+ 'username',
+ 'email_display',
+ 'full_name_display',
+ 'is_staff',
+ 'is_active',
+ 'date_joined',
+ 'last_login',
+ 'profile_photo_tag',
+ 'posts_count_display'
+ )
list_filter = ('is_staff', 'is_superuser', 'groups', 'is_active')
search_fields = ('username',)
ordering = ('-date_joined',)
@@ -37,16 +45,16 @@ class MyUserAdmin(BaseUserAdmin):
)
def email_display(self, queryset):
- return format_html('{}'.format(queryset.email))
+ return format_html(f'{queryset.email}')
email_display.short_description = "Email"
def full_name_display(self, queryset):
- return format_html('{}'.format(queryset.name))
+ return format_html(f'{queryset.name}')
full_name_display.short_description = "Name"
- def articles_count_display(self, queryset):
- return queryset.articles.count()
+ def posts_count_display(self, queryset):
+ return queryset.posts.count()
- articles_count_display.short_description = "Articles"
+ posts_count_display.short_description = "Articles"
diff --git a/bloggy/apps.py b/bloggy/apps.py
index 52eb721..1d36b76 100644
--- a/bloggy/apps.py
+++ b/bloggy/apps.py
@@ -5,4 +5,4 @@ class MyAppConfig(AppConfig):
name = 'bloggy'
def ready(self):
- import bloggy.signals
+ pass
diff --git a/bloggy/context_processors.py b/bloggy/context_processors.py
index 226976f..33b6f51 100644
--- a/bloggy/context_processors.py
+++ b/bloggy/context_processors.py
@@ -1,3 +1,5 @@
+import json
+
from django.http import HttpRequest
from bloggy import settings
@@ -8,87 +10,30 @@ def seo_attrs(request: HttpRequest):
Arguments:
request {HttpRequest} -- request object
"""
-
- seo = {
- 'seo_site_name': settings.SITE_TITLE,
- 'seo_title': settings.SITE_TAGLINE,
- 'seo_description': 'Elevate your coding skills with StackTips. Learn through articles, courses, and quizzes on StackTips',
- 'seo_keywords': 'stacktips, stack tips, python course, java tutorials, spring tutorials, spring boot tutorials, maven course for bignners, practice quiz, aws practice quiz, aws practice test',
- 'og_image': '',
- 'seo_image': settings.SITE_LOGO
- }
-
- if request.path == "/quizzes":
- seo["seo_title"] = "Interactive Quizzes"
- seo['seo_description'] = "Use bloggy to test your coding skills and gauge your learning progress in a fun way"
- seo['seo_keywords'] = 'stacktips quizzes, stack tips bloggy, quizlzet, take a quiz, practice test, aws quiz, python test, advance python test, aws practitioner test, CLF-C01 AWS test, aws CLF-C01 quiz'
-
- elif request.path == "/courses":
- seo["seo_title"] = "Courses"
- seo['seo_description'] = "StackTips offers free Programming Courses to teach you the fundamentals of popular programming languages such as Java, Python, HTML, JavaScript and CSS"
- seo['seo_keywords'] = 'stacktips courses, stack tips courses, free courses, learn python for free, python course, maven for beginner, maven courses, maven basics courrse'
-
- elif request.path == "/articles":
- seo["seo_title"] = "Tutorials"
- seo['seo_description'] = "Learn from beginner basics to advanced tutorials on Java, Spring, Python, Android and Git."
- seo['seo_keywords'] = 'stacktips tutorials, stack tips, free tutorials, java tutorials, spring tutorials, spring boot tutorials, django tutorials, python tutorials, git tutorials, maven tutorials, android tutorials '
-
- elif request.path == "/topics":
- seo["seo_title"] = "Categories"
- seo['seo_description'] = "Browse from collection of tutorials and courses organized by categories to make learning easy and accessible."
- seo['seo_keywords'] = "stacktips categories, stack tips topics, browse by topics, stacktips categories, tutorial categories, tutorial categories, all tutorials by category, android, django, python, django, spring, spring boot, git"
-
- elif request.path == "/cookie-policy":
- seo["seo_title"] = "Cookie policy"
- seo['seo_description'] = "Our Cookies Policy explains what Cookies are and how We use them."
-
- elif request.path == "/privacy":
- seo["seo_title"] = "Privacy policy"
- seo['seo_description'] = "This page explains our privacy policy"
-
- elif request.path == "/terms-of-service":
- seo["seo_title"] = "Terms of service"
- seo['seo_description'] = "This page outlines the legal terms of stacktips.com, its sub-domains, and all associated web and/or mobile apps."
-
- elif request.path == "/comment-policy":
- seo["seo_title"] = "Comment policy"
- seo['seo_description'] = "This page lists some of the simple ground rules for commenting on this site"
-
- elif request.path == "/contact":
- seo["seo_title"] = "Get in touch"
- seo['seo_description'] = "If you have any questions or have suggestions to improve the quality, let us know."
-
- elif request.path == "/about":
- seo["seo_title"] = "About us"
- seo['seo_description'] = "We aim to provide developer-friendly ways to learn programming. With articles, programming course, and quizzes, we aim to teach in the ways developers learn best."
-
- elif request.path == "/guestbook":
- seo["seo_title"] = "Guestbook"
- seo['seo_description'] = "You have a chance to be creative. Indulge me by leaving a greeting below."
-
- elif request.path == "/newsletter":
- seo["seo_title"] = "Newsletter"
- seo['seo_description'] = "Get access to our fortnightly newsletter with articles, courses, and quizzes. You may unsubscribe at any time using the link in our newsletter."
-
- elif request.path == "/login":
- seo["seo_title"] = "Login"
- seo['seo_description'] = "To keep connected with us, please log in with your email address and account password."
- seo['seo_keywords'] = "stacktips account, login to stacktips, join stacktips, your stack tips account, join for free, login, sign in, login stack tips, signin to stacktips.com, login to stacktips"
-
- elif request.path == "/register":
- seo["seo_title"] = "Create your free account!"
- seo['seo_description'] = "Register to get exclusive access to articles, live-demos, or courses, We aim to teach in the ways developers learn best."
- seo['seo_keywords'] = "stacktips account, create free stacktips account, signup new account, free account, stack tips account free, join stacktips, stacktips free account, register free account, new account stack tips"
-
- elif request.path == "/authors":
- seo["seo_title"] = "Our Authors @ StackTips"
- seo['seo_description'] = "List of the authors on StackTips website."
+ with open('seo_settings.json', 'r', encoding='utf-8') as seo_file:
+ seo_settings = json.load(seo_file)
+ # Default SEO attributes
+ seo = {
+ 'site_name': settings.SITE_TITLE,
+ 'meta_title': settings.SITE_TAGLINE,
+ 'meta_description': settings.SITE_DESCRIPTION,
+ 'meta_image': settings.SITE_LOGO
+ }
+
+ # Get SEO attributes based on the request path
+ request_path = request.path
+ if request_path in seo_settings:
+ seo.update(seo_settings[request_path])
return seo
def app_settings(request: HttpRequest):
+ """
+ returns app settings
+ """
return {
- "DEVELOPMENT_MODE": settings.DEVELOPMENT_MODE,
+ "LOAD_GOOGLE_TAG_MANAGER": settings.LOAD_GOOGLE_TAG_MANAGER,
+ "LOAD_GOOGLE_ADS": settings.LOAD_GOOGLE_ADS,
"ASSETS_DOMAIN": settings.ASSETS_DOMAIN
}
diff --git a/bloggy/forms/comment_form.py b/bloggy/forms/comment_form.py
index 5dfc355..2421e06 100644
--- a/bloggy/forms/comment_form.py
+++ b/bloggy/forms/comment_form.py
@@ -1,8 +1,10 @@
-from bloggy.models.comment import Comment
from django import forms
+from bloggy.models.comment import Comment
+
class CommentForm(forms.ModelForm):
+
class Meta:
model = Comment
- fields = ('use_name', 'user_email', 'comment_content')
\ No newline at end of file
+ fields = ('use_name', 'user_email', 'comment_content')
diff --git a/bloggy/forms/edit_profile_form.py b/bloggy/forms/edit_profile_form.py
index dd79cbd..68c789b 100644
--- a/bloggy/forms/edit_profile_form.py
+++ b/bloggy/forms/edit_profile_form.py
@@ -1,6 +1,7 @@
from django import forms
from django.forms import ClearableFileInput
-from bloggy.models import MyUser
+
+from bloggy.models import User
class NonClearableFileInput(ClearableFileInput):
@@ -9,7 +10,7 @@ class NonClearableFileInput(ClearableFileInput):
class EditProfileForm(forms.ModelForm):
class Meta:
- model = MyUser
+ model = User
fields = [
'profile_photo',
'name',
diff --git a/bloggy/forms/signup_form.py b/bloggy/forms/signup_form.py
index 564cce6..139efb7 100644
--- a/bloggy/forms/signup_form.py
+++ b/bloggy/forms/signup_form.py
@@ -1,33 +1,46 @@
+import logging
from django.contrib.auth.forms import UserCreationForm
-
-from bloggy.models import MyUser
+from django.core.exceptions import ValidationError
+from bloggy.models import User
from django import forms
+logger = logging.getLogger(__name__)
+
class SignUpForm(UserCreationForm):
+ honeypot = forms.CharField(required=False, widget=forms.HiddenInput)
+
class Meta:
- model = MyUser
+ model = User
fields = ('name', 'email', 'password1', 'password2')
def save(self, commit=True):
user = super().save(commit=False) # Call the parent class's save method
# Generate the username based on the user's name (you can use your custom function here)
user.username = self.generate_unique_username(self.cleaned_data['name'])
- user.is_active = True
+ user.is_active = False
user.is_staff = False
if commit:
user.save()
return user
- def generate_unique_username(self, name):
+ def clean_honeypot(self):
+ honeypot_value = self.cleaned_data.get('honeypot')
+ if honeypot_value:
+ logger.error("ERROR: Honeypot validation error!")
+ raise ValidationError("Oops! Looks like you're not a human!")
+ return honeypot_value
+
+ @staticmethod
+ def generate_unique_username(name):
# Convert the user's name to a lowercase username with underscores
base_username = name.lower().replace(' ', '_')
# Check if the base_username is unique, if not, append a number until it is
username = base_username
count = 1
- while MyUser.objects.filter(username=username).exists():
+ while User.objects.filter(username=username).exists():
username = f"{base_username}{count}"
count += 1
diff --git a/bloggy/forms/update_password_form.py b/bloggy/forms/update_password_form.py
index 50021cb..2b1db31 100644
--- a/bloggy/forms/update_password_form.py
+++ b/bloggy/forms/update_password_form.py
@@ -1,11 +1,11 @@
from django import forms
from django.forms import CharField, PasswordInput
-from bloggy.models import MyUser
+from bloggy.models import User
class UpdatePasswordForm(forms.BaseForm):
- model = MyUser
+ model = User
error_css_class = 'has-error'
error_messages = {'password_incorrect': "The old password is not correct. Try again."}
diff --git a/bloggy/management/commands/.DS_Store b/bloggy/management/commands/.DS_Store
new file mode 100644
index 0000000..a22a8b6
Binary files /dev/null and b/bloggy/management/commands/.DS_Store differ
diff --git a/bloggy/management/commands/runseed.py b/bloggy/management/commands/runseed.py
new file mode 100644
index 0000000..b8658bc
--- /dev/null
+++ b/bloggy/management/commands/runseed.py
@@ -0,0 +1,32 @@
+from django.core.management import call_command
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ help = 'Importing demo contents'
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def add_arguments(self, parser):
+ parser.add_argument('--dir', type=str, help="File path to import, e.g. ~/bloggy/demo_content")
+
+ def handle(self, *args, **options):
+ file_path = options['dir']
+
+ commands = [
+ ('seed_users', 'users.csv'),
+ ('seed_categories', 'categories.csv'),
+ ('seed_posts', 'posts.csv'),
+ ('seed_pages', 'pages.csv'),
+ ('seed_redirectrules', 'redirect_rules.csv'),
+ ('update_category_count', None),
+ ]
+
+ for command, file in commands:
+ if file:
+ call_command(command, f'--file={file_path}/{file}')
+ else:
+ call_command(command)
+
+ self.stdout.write(self.style.SUCCESS("Import Complete!"))
diff --git a/bloggy/management/commands/seed_categories.py b/bloggy/management/commands/seed_categories.py
new file mode 100644
index 0000000..b1e2e10
--- /dev/null
+++ b/bloggy/management/commands/seed_categories.py
@@ -0,0 +1,36 @@
+import csv
+
+from django.core.management.base import BaseCommand
+from django.utils.text import slugify
+
+from bloggy.models import Category
+
+
+class Command(BaseCommand):
+ help = 'Importing categories'
+
+ def add_arguments(self, parser):
+ parser.add_argument('-f', '--file', type=str,
+ help="File path to import, e.g. ~/bloggy/demo_content/categories.csv")
+
+ def handle(self, *args, **options):
+ file_path = options['file']
+
+ counter = 0
+ with open(file_path, encoding="utf-8") as f:
+ reader = csv.reader(f)
+ print('Importing categories from file', file_path)
+
+ for index, row in enumerate(reader):
+ if index > 0:
+ counter = counter + 1
+ Category.objects.get_or_create(
+ title=row[0],
+ slug=slugify(row[1]),
+ description=row[2],
+ logo=row[3],
+ color=row[4],
+ publish_status=row[5]
+ )
+
+ self.stdout.write(self.style.SUCCESS(f"Imported %s categories" % counter))
diff --git a/bloggy/management/commands/seed_pages.py b/bloggy/management/commands/seed_pages.py
new file mode 100644
index 0000000..936823b
--- /dev/null
+++ b/bloggy/management/commands/seed_pages.py
@@ -0,0 +1,35 @@
+import csv
+
+from django.core.management.base import BaseCommand
+from bloggy.models.page import Page
+
+
+class Command(BaseCommand):
+ help = 'Importing pages'
+
+ def add_arguments(self, parser):
+ parser.add_argument('-f', '--file', type=str,
+ help="File path to import, e.g. ~/bloggy/demo_content/pages.csv")
+
+ def handle(self, *args, **options):
+ file_path = options['file']
+
+ counter = 0
+ with open(file_path, encoding="utf-8") as f:
+ reader = csv.reader(f)
+ print('Importing pages from file', file_path)
+ for index, row in enumerate(reader):
+ if index > 0:
+ counter = counter + 1
+ Page.objects.get_or_create(
+ title=row[0],
+ url=row[1],
+ publish_status=row[2],
+ meta_title=row[3],
+ meta_description=row[4],
+ meta_keywords=row[5],
+ excerpt=row[6],
+ content=row[7],
+ )
+
+ self.stdout.write(self.style.SUCCESS(f"Imported %s pages" % counter))
diff --git a/bloggy/management/commands/seed_posts.py b/bloggy/management/commands/seed_posts.py
new file mode 100644
index 0000000..024ad2d
--- /dev/null
+++ b/bloggy/management/commands/seed_posts.py
@@ -0,0 +1,51 @@
+import csv
+
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from django.utils.text import slugify
+
+from bloggy.models import Category, Post, User
+
+
+class Command(BaseCommand):
+ help = 'Importing posts'
+
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+
+ def add_arguments(self, parser):
+ parser.add_argument('-f', '--file', type=str,
+ help="File path to import, e.g. ~/bloggy/demo_content/posts.csv")
+
+ def handle(self, *args, **options):
+ file_path = options['file']
+
+ counter = 0
+ with open(file_path, encoding="utf-8") as f:
+ reader = csv.reader(f)
+ print('Importing articles from file', file_path)
+ for index, row in enumerate(reader):
+ if index > 0:
+ counter = counter + 1
+ slug = slugify(row[0])
+ article = Post.objects.get_or_create(
+ title=row[0],
+ slug=slug,
+ publish_status=row[1],
+ excerpt=row[2],
+ difficulty=row[3],
+ is_featured=row[4],
+ content=row[5],
+ video_id=row[8],
+ post_type=row[9],
+ template_type=row[10],
+ published_date=timezone.now(),
+ author=User.objects.get(id=row[7]),
+ )
+
+ categories = Category.objects.filter(slug__in=row[11].split(",")).all()
+ saved_article = Post.objects.get(slug=slug)
+ saved_article.category.set(categories)
+ saved_article.save()
+
+ self.stdout.write(self.style.SUCCESS(f"Imported %s articles" % counter))
diff --git a/bloggy/management/commands/seed_redirectrules.py b/bloggy/management/commands/seed_redirectrules.py
new file mode 100644
index 0000000..a37e6a7
--- /dev/null
+++ b/bloggy/management/commands/seed_redirectrules.py
@@ -0,0 +1,34 @@
+import csv
+
+from django.core.management.base import BaseCommand
+from django.utils.text import slugify
+
+from bloggy.models import Category, RedirectRule
+
+
+class Command(BaseCommand):
+ help = 'Importing redirect rules'
+
+ def add_arguments(self, parser):
+ parser.add_argument('-f', '--file', type=str,
+ help="File path to import, e.g. ~/bloggy/demo_content/redirect_rules.csv")
+
+ def handle(self, *args, **options):
+ file_path = options['file']
+
+ counter = 0
+ with open(file_path, encoding="utf-8") as f:
+ reader = csv.reader(f)
+ print('Importing redirect rules', file_path)
+
+ for index, row in enumerate(reader):
+ if index > 0:
+ counter = counter + 1
+ RedirectRule.objects.get_or_create(
+ from_url=row[0],
+ to_url=row[1],
+ status_code=row[2],
+ note=row[3]
+ )
+
+ self.stdout.write(self.style.SUCCESS(f"%s redirect rules imported" % counter))
diff --git a/bloggy/management/commands/seed_users.py b/bloggy/management/commands/seed_users.py
new file mode 100644
index 0000000..d3be481
--- /dev/null
+++ b/bloggy/management/commands/seed_users.py
@@ -0,0 +1,59 @@
+import csv
+
+from django.contrib.auth.hashers import make_password
+from django.core.management.base import BaseCommand
+from django.db import IntegrityError
+from django.utils.text import slugify
+
+from bloggy.models import User
+
+
+class Command(BaseCommand):
+ help = 'Importing users'
+
+ def add_arguments(self, parser):
+ parser.add_argument('-f', '--file', type=str,
+ help="File path to import, e.g. ~/bloggy/demo_content/users.csv")
+
+ def handle(self, *args, **options):
+ file_path = options['file']
+
+ counter = 0
+ with open(file_path, encoding="utf-8") as f:
+ reader = csv.reader(f)
+ print('Importing categories from file', file_path)
+
+ for index, row in enumerate(reader):
+ if index > 0:
+ counter = counter + 1
+
+ username = self.format_username(row[1])
+ try:
+ new_user = User.objects.create_user(
+ name=row[3],
+ password=make_password(row[0]),
+ username=username,
+ email=row[2])
+
+ except IntegrityError:
+ print("user exist")
+ new_user = User.objects.get(email=row[2])
+ finally:
+ print("user updating")
+
+ new_user.is_staff = row[4]
+ new_user.is_active = row[5]
+ new_user.website = row[6]
+ new_user.twitter = row[7]
+ new_user.linkedin = row[8]
+ new_user.youtube = row[9]
+ new_user.github = row[10]
+ new_user.bio = row[11]
+ new_user.save()
+ print("User updated {}", new_user)
+
+ self.stdout.write(self.style.SUCCESS(f"Imported %s users" % counter))
+
+ def format_username(self, username):
+ formatted_username = slugify(username.replace(".", "_").lower())
+ return formatted_username.replace("-", "_")
diff --git a/bloggy/management/commands/update_category_count.py b/bloggy/management/commands/update_category_count.py
index 829e6ef..af3138e 100644
--- a/bloggy/management/commands/update_category_count.py
+++ b/bloggy/management/commands/update_category_count.py
@@ -1,25 +1,22 @@
from django.core.management.base import BaseCommand
from django.db import transaction
-from bloggy.models import Category, Article
+from bloggy.models import Category, Post
class Command(BaseCommand):
help = 'Update Category Count'
def __init__(self, *args, **kwargs):
- super(Command, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
def handle(self, *args, **options):
categories = Category.objects.select_for_update().all()
with transaction.atomic():
for category in categories:
- article_count = Article.objects.all().filter(category=category).count()
+ article_count = Post.objects.all().filter(category=category).count()
if article_count > 0:
category.article_count = article_count
category.save()
- print("{\"" + category.title + "\": {\"article_count\":" + str(article_count) + "}}")
- pass
-
- self.stdout.write(self.style.SUCCESS(f'Successfully updated'))
+ self.stdout.write(self.style.SUCCESS('Updated category count.'))
diff --git a/bloggy/middleware/page_not_found.py b/bloggy/middleware/page_not_found.py
deleted file mode 100644
index bd619d6..0000000
--- a/bloggy/middleware/page_not_found.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import logging
-
-from django.http import HttpResponsePermanentRedirect
-from django.utils.deprecation import MiddlewareMixin
-from bloggy import settings
-
-logger = logging.getLogger(__name__)
-
-fallback_404_url_mapping_new_site = {
- "/articles/how-to-configure-pojos-with-java-collection-attributes": "/articles/configure-pojos-with-java-collection-attributes-in-spring",
- "/courses/maven": "/courses/maven-for-beginners"
-}
-
-
-class PageNotFoundMiddleware(MiddlewareMixin):
-
- def process_response(self, request, response):
- request_path = request.path
-
- # Don't do anything for /api endpoints
- if request_path.startswith("/api/"):
- return response
-
- if response.status_code == 404:
- logger.warning("ERROR 404:: {}".format(request_path))
- if request_path in fallback_404_url_mapping_new_site:
- new_path = fallback_404_url_mapping_new_site[request_path]
- logger.warning("ERROR 404 Fallback:: {} ==> {}".format(request_path, new_path))
- return HttpResponsePermanentRedirect(settings.SITE_URL + new_path)
-
- return response
diff --git a/bloggy/middleware/redirect.py b/bloggy/middleware/redirect.py
new file mode 100644
index 0000000..19d6044
--- /dev/null
+++ b/bloggy/middleware/redirect.py
@@ -0,0 +1,30 @@
+import logging
+
+from django.http import HttpResponsePermanentRedirect
+from django.utils.deprecation import MiddlewareMixin
+
+from bloggy import settings
+from bloggy.models import RedirectRule
+
+logger = logging.getLogger(__name__)
+
+
+class RedirectMiddleware(MiddlewareMixin):
+
+ def process_response(self, request, response):
+ request_path = request.path
+
+ # Don't do anything for /api endpoints
+ if request_path.startswith("/api/"):
+ return response
+
+ if response.status_code == 404:
+ logger.warning("ERROR 404:: %s", request_path)
+ redirect_rule = RedirectRule.objects.filter(source__exact=request_path).first()
+
+ if redirect_rule:
+ logger.warning("Explicit redirect rule found %s ==> %s", redirect_rule.source,
+ redirect_rule.destination)
+ return HttpResponsePermanentRedirect(settings.SITE_URL + redirect_rule.destination)
+
+ return response
diff --git a/bloggy/middleware/slash_middleware.py b/bloggy/middleware/slash_middleware.py
index 2292af1..d208c4a 100644
--- a/bloggy/middleware/slash_middleware.py
+++ b/bloggy/middleware/slash_middleware.py
@@ -1,8 +1,9 @@
+from urllib.parse import quote
+
from django import http
from django import urls
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
-from urllib.parse import quote
class AppendOrRemoveSlashMiddleware(MiddlewareMixin):
@@ -57,7 +58,7 @@ def process_response(self, request, response):
def generate_url(request, path):
if request.get_host():
- new_url = "%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), quote(path))
+ new_url = f"{request.is_secure() and 'https' or 'http'}://{request.get_host()}{quote(path)}"
else:
new_url = quote(path)
if request.GET:
diff --git a/bloggy/middleware/wp_redirect.py b/bloggy/middleware/wp_redirect.py
deleted file mode 100644
index d755f7f..0000000
--- a/bloggy/middleware/wp_redirect.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# myproject.middleware.py
-import logging
-from django.http import HttpResponsePermanentRedirect
-from django.urls import reverse
-from django.utils.deprecation import MiddlewareMixin
-from bloggy import settings
-
-logger = logging.getLogger(__name__)
-
-fallback_404_url_mapping_old_site = {
- "/tutorials/spring/how-to-configure-pojos-with-java-collection-attributes": "/articles/configure-pojos-with-java-collection-attributes-in-spring",
- "/android/download-and-display-image-in-android-gridview": "/articles/download-and-display-image-in-android-gridview",
- "/article/how-to-format-your-post": "/how-to-format-your-post",
- "/android/android-framelayout-example": "/articles/android-framelayout-example",
- "/interview-questions": "/articles",
- "/courses/maven": "/courses/maven-for-beginners",
- "/how-to-format-your-post": "/contribute",
- "/article/download-and-display-image-in-android-gridview": "/articles/download-and-display-image-in-android-gridview"
-}
-
-paths_to_replace = [
- "/tutorials/java/spring/",
- "/tutorials/android/",
- "/tutorials/spring-boot/",
- "/tutorials/spring/",
- "/tutorials/laravel/",
- "/tutorials/php/",
- "/tutorials/git/",
- "/tutorials/html5/",
- "/tutorials/bootstrap/",
- "/tutorials/java/",
- "/tutorials/xamarin/",
- "/tutorials/react/",
- "/tutorials/react-native/",
- "/tutorials/wordpress/",
- "/tutorials/json/",
- "/tutorials/design-patterns/",
- "/tutorials/sencha-touch/",
- "/tutorials/ibm-worklight/",
- "/tutorials/phonegap/",
- "/tutorials/ios/",
- "/tutorials/seo/",
- "/tutorials/se-concepts/",
- "/tutorials/servlets/",
- "/tutorials/struts/",
- "/tutorials/c/",
- "/tutorials/struts/",
- "/tutorials/libgdx/",
- "/how-to/",
- "/blog/",
-]
-
-
-class OldUrlRedirectMiddleware(MiddlewareMixin):
-
- def process_response(self, request, response):
- request_path = request.path
-
- # Don't do anything for /api endpoints
- if request_path.startswith("/api/"):
- return response
-
- if request_path.startswith("/tutorials/blackberry/") or request_path.startswith("/tutorials/j2me/"):
- new_path = reverse("articles")
- logger.warning("WPRedirect:: {}->{}".format(request_path, new_path))
- return HttpResponsePermanentRedirect(settings.SITE_URL + new_path)
-
- if request_path in fallback_404_url_mapping_old_site:
- new_path = fallback_404_url_mapping_old_site[request_path]
- logger.warning("WPRedirect:: {}->{}".format(request_path, new_path))
- return HttpResponsePermanentRedirect(settings.SITE_URL + new_path)
-
- if request_path.startswith("/courses/maven/"):
- new_path = request_path.replace(request_path, "/courses/maven-for-beginners/")
- logger.warning("WPRedirect:: {}->{}".format(request_path, new_path))
- return HttpResponsePermanentRedirect(settings.SITE_URL + new_path)
-
- for ptr in paths_to_replace:
- if request_path.startswith(ptr):
- new_path = request_path.replace(ptr, "/articles/")
- logger.warning("WPRedirect:: {}->{}".format(request_path, new_path))
- return HttpResponsePermanentRedirect(settings.SITE_URL + new_path)
-
- return response
diff --git a/bloggy/migrations/0001_initial.py b/bloggy/migrations/0001_initial.py
index a6255ea..914d7e5 100644
--- a/bloggy/migrations/0001_initial.py
+++ b/bloggy/migrations/0001_initial.py
@@ -1,9 +1,10 @@
-# Generated by Django 4.2.6 on 2023-10-21 21:44
+# Generated by Django 4.2.6 on 2023-11-07 22:41
-import bloggy.models.article
import bloggy.models.categories
import bloggy.models.course
import bloggy.models.mixin.ResizeImageMixin
+import bloggy.models.post
+import bloggy.models.quizzes
import bloggy.models.user
import colorfield.fields
from django.conf import settings
@@ -24,7 +25,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='MyUser',
+ name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
@@ -32,7 +33,7 @@ class Migration(migrations.Migration):
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
- ('name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ ('name', models.CharField(blank=True, max_length=150, verbose_name='full name')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('profile_photo', models.ImageField(blank=True, null=True, storage=bloggy.models.user.select_storage, upload_to=bloggy.models.user.upload_profile_image)),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
@@ -48,57 +49,60 @@ class Migration(migrations.Migration):
],
options={
'verbose_name_plural': 'Users',
+ 'db_table': 'bloggy_user',
'ordering': ['username'],
},
bases=(bloggy.models.mixin.ResizeImageMixin.ResizeImageMixin, models.Model),
),
migrations.CreateModel(
- name='Article',
+ name='Category',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
+ ('meta_title', models.CharField(blank=True, help_text='Meta Title', max_length=120, null=True)),
+ ('meta_description', models.TextField(blank=True, help_text='Meta Description', null=True)),
+ ('meta_keywords', models.CharField(blank=True, help_text='Meta Keywords', max_length=300, null=True)),
('updated_date', models.DateTimeField(auto_now=True, null=True)),
- ('display_order', models.IntegerField(default=0, help_text='Display order', null=True)),
- ('title', models.CharField(help_text='Enter title', max_length=300)),
- ('keywords', models.CharField(blank=True, help_text='Enter title', max_length=300, null=True)),
- ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
- ('published_date', models.DateTimeField(blank=True, null=True)),
+ ('title', models.CharField(help_text='Enter title', max_length=150)),
+ ('article_count', models.IntegerField(default=0)),
('slug', models.SlugField(help_text='Enter slug', max_length=150, unique=True)),
- ('excerpt', models.CharField(blank=True, help_text='Enter excerpt', max_length=500, null=True)),
- ('video_id', models.CharField(blank=True, help_text='YouTube Video ID', max_length=100, null=True)),
- ('difficulty', models.CharField(blank=True, choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advance', 'Advance')], default='beginner', help_text='Select difficulty', max_length=20, null=True, verbose_name='Difficulty level')),
- ('post_type', models.CharField(blank=True, choices=[('article', 'Article'), ('quiz', 'Quiz'), ('lesson', 'Lesson')], default='article', help_text='Post type', max_length=20, null=True, verbose_name='Post type')),
- ('template_type', models.CharField(blank=True, choices=[('standard', 'Standard'), ('cover', 'Cover'), ('naked', 'Naked'), ('full', 'Full')], default='standard', help_text='Template type', max_length=20, null=True, verbose_name='Template type')),
- ('duration', models.IntegerField(default='1', help_text='Duration in minutes. For articles, it will be calculated automatically.')),
- ('is_featured', models.BooleanField(default=False, help_text='Should this story be featured on site?')),
- ('content', models.TextField(help_text='Post content', null=True)),
- ('thumbnail', models.ImageField(blank=True, null=True, upload_to=bloggy.models.article.upload_thumbnail_image)),
- ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)),
+ ('description', models.TextField(blank=True, help_text='Enter description', max_length=1000, null=True)),
+ ('logo', models.ImageField(null=True, upload_to=bloggy.models.categories.upload_logo_image)),
+ ('color', colorfield.fields.ColorField(default='#1976D2', image_field=None, max_length=25, samples=None)),
+ ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
],
options={
- 'verbose_name': 'article',
- 'verbose_name_plural': 'articles',
+ 'verbose_name': 'category',
+ 'verbose_name_plural': 'categories',
'ordering': ['title'],
},
),
migrations.CreateModel(
- name='Category',
+ name='Course',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
+ ('meta_title', models.CharField(blank=True, help_text='Meta Title', max_length=120, null=True)),
+ ('meta_description', models.TextField(blank=True, help_text='Meta Description', null=True)),
+ ('meta_keywords', models.CharField(blank=True, help_text='Meta Keywords', max_length=300, null=True)),
('updated_date', models.DateTimeField(auto_now=True, null=True)),
- ('title', models.CharField(help_text='Enter title', max_length=150)),
- ('article_count', models.IntegerField(default=0)),
+ ('title', models.CharField(help_text='Enter title', max_length=300)),
('slug', models.SlugField(help_text='Enter slug', max_length=150, unique=True)),
- ('description', models.TextField(blank=True, help_text='Enter description', max_length=1000, null=True)),
- ('logo', models.ImageField(null=True, upload_to=bloggy.models.categories.upload_logo_image)),
- ('color', colorfield.fields.ColorField(default='#1976D2', image_field=None, max_length=25, samples=None)),
- ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
+ ('excerpt', models.CharField(blank=True, help_text='Enter excerpt', max_length=500, null=True)),
+ ('display_order', models.IntegerField(default=0, help_text='Display order', null=True)),
+ ('published_date', models.DateTimeField(blank=True, null=True)),
+ ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
+ ('difficulty', models.CharField(blank=True, choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advance', 'advance')], default='easy', help_text='Select difficulty', max_length=20, null=True, verbose_name='Difficulty level')),
+ ('is_featured', models.BooleanField(default=False, help_text='Should this story be featured on site?')),
+ ('description', models.TextField(help_text='Enter answer', null=True)),
+ ('thumbnail', models.ImageField(blank=True, null=True, upload_to=bloggy.models.course.upload_thumbnail_image)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to=settings.AUTH_USER_MODEL)),
+ ('category', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='bloggy.category')),
],
options={
- 'verbose_name': 'category',
- 'verbose_name_plural': 'categories',
- 'ordering': ['title'],
+ 'verbose_name': 'course',
+ 'verbose_name_plural': 'courses',
+ 'ordering': ['-display_order'],
},
),
migrations.CreateModel(
@@ -130,24 +134,95 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
- name='PostViews',
+ name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
- ('ip_address', models.GenericIPAddressField(default='0.0.0.0')),
+ ('meta_title', models.CharField(blank=True, help_text='Meta Title', max_length=120, null=True)),
+ ('meta_description', models.TextField(blank=True, help_text='Meta Description', null=True)),
+ ('meta_keywords', models.CharField(blank=True, help_text='Meta Keywords', max_length=300, null=True)),
('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('title', models.CharField(help_text='Enter title', max_length=300)),
+ ('excerpt', models.CharField(blank=True, help_text='Enter excerpt', max_length=500, null=True)),
+ ('url', models.CharField(help_text='Enter url', max_length=150, unique=True)),
+ ('content', models.TextField(help_text='Post content', null=True)),
+ ('published_date', models.DateTimeField(blank=True, null=True)),
+ ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
],
+ options={
+ 'verbose_name': 'Page',
+ 'verbose_name_plural': 'Pages',
+ },
),
migrations.CreateModel(
- name='Votes',
+ name='Quiz',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
+ ('meta_title', models.CharField(blank=True, help_text='Meta Title', max_length=120, null=True)),
+ ('meta_description', models.TextField(blank=True, help_text='Meta Description', null=True)),
+ ('meta_keywords', models.CharField(blank=True, help_text='Meta Keywords', max_length=300, null=True)),
+ ('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('title', models.CharField(help_text='Enter title', max_length=300)),
+ ('slug', models.SlugField(help_text='Enter slug', max_length=150, unique=True)),
+ ('excerpt', models.CharField(blank=True, help_text='Enter excerpt', max_length=500, null=True)),
+ ('display_order', models.IntegerField(default=0, help_text='Display order', null=True)),
+ ('published_date', models.DateTimeField(blank=True, null=True)),
+ ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
+ ('difficulty', models.CharField(blank=True, choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advance', 'advance')], default='easy', help_text='Select difficulty', max_length=20, null=True, verbose_name='Difficulty level')),
+ ('is_featured', models.BooleanField(default=False, help_text='Should this story be featured on site?')),
+ ('content', models.TextField(help_text='Post content', null=True)),
+ ('thumbnail', models.ImageField(blank=True, null=True, upload_to=bloggy.models.quizzes.upload_thumbnail_image)),
+ ('duration', models.IntegerField(default='1', help_text='Duration in minutes. For articles, it will be calculated automatically.')),
+ ('category', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='bloggy.category')),
+ ],
+ options={
+ 'verbose_name': 'Quiz',
+ 'verbose_name_plural': 'Quizzes',
+ 'ordering': ['title'],
+ },
+ ),
+ migrations.CreateModel(
+ name='RedirectRule',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
+ ('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('source', models.CharField(help_text='Enter from url', max_length=300)),
+ ('destination', models.CharField(help_text='Enter to url', max_length=300)),
+ ('status_code', models.IntegerField(blank=True, choices=[(301, '301 Moved Permanently'), (307, '307 Temporary Redirect')], default='standard', help_text='Redirect type', null=True, verbose_name='Redirect type')),
+ ('is_regx', models.BooleanField(default=False, help_text='Is this regx?', null=True)),
+ ('note', models.CharField(blank=True, help_text='Enter note', max_length=500, null=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='Vote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
- ('post_id', models.IntegerField(help_text='Post id')),
- ('post_type', models.CharField(choices=[('question', 'question'), ('article', 'article')], help_text='Select content type', max_length=20, verbose_name='Content type')),
('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('post_id', models.IntegerField(help_text='Post id')),
+ ('post_type', models.CharField(choices=[['post', 'Post'], ['lesson', 'Lesson']], help_text='Select content type', max_length=20, verbose_name='Content type')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
+ options={
+ 'verbose_name': 'Vote',
+ 'verbose_name_plural': 'Votes',
+ },
+ ),
+ migrations.CreateModel(
+ name='Bookmark',
+ fields=[
+ ('vote_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bloggy.vote')),
+ ],
+ options={
+ 'verbose_name': 'Bookmark',
+ 'verbose_name_plural': 'Bookmarks',
+ },
+ bases=('bloggy.vote',),
),
migrations.CreateModel(
name='VerificationToken',
@@ -172,7 +247,7 @@ class Migration(migrations.Migration):
('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
('updated_date', models.DateTimeField(auto_now=True, null=True)),
('score', models.FloatField()),
- ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bloggy.article')),
+ ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bloggy.quiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
@@ -204,7 +279,7 @@ class Migration(migrations.Migration):
('description', models.TextField(blank=True, help_text='Enter description', null=True)),
('explanation', models.TextField(blank=True, help_text='Enter explanation', null=True)),
('type', models.CharField(blank=True, choices=[('binary', 'binary'), ('multiple', 'multiple')], default='binary', help_text='Select type of question', max_length=20, null=True, verbose_name='Question type')),
- ('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='bloggy.article')),
+ ('quiz', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='bloggy.quiz')),
],
),
migrations.CreateModel(
@@ -217,42 +292,36 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
- name='PostMeta',
- fields=[
- ('id', models.AutoField(primary_key=True, serialize=False)),
- ('meta_key', models.SlugField(help_text='Enter key', max_length=150)),
- ('meta_value', models.TextField(help_text='Enter value', null=True)),
- ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bloggy.article')),
- ],
- options={
- 'verbose_name': 'Post metadata',
- 'verbose_name_plural': 'Post metadata',
- },
- ),
- migrations.CreateModel(
- name='Course',
+ name='Post',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
+ ('meta_title', models.CharField(blank=True, help_text='Meta Title', max_length=120, null=True)),
+ ('meta_description', models.TextField(blank=True, help_text='Meta Description', null=True)),
+ ('meta_keywords', models.CharField(blank=True, help_text='Meta Keywords', max_length=300, null=True)),
('updated_date', models.DateTimeField(auto_now=True, null=True)),
- ('display_order', models.IntegerField(default=0, help_text='Display order', null=True)),
('title', models.CharField(help_text='Enter title', max_length=300)),
- ('keywords', models.CharField(blank=True, help_text='Enter title', max_length=300, null=True)),
- ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
- ('published_date', models.DateTimeField(blank=True, null=True)),
('slug', models.SlugField(help_text='Enter slug', max_length=150, unique=True)),
('excerpt', models.CharField(blank=True, help_text='Enter excerpt', max_length=500, null=True)),
+ ('display_order', models.IntegerField(default=0, help_text='Display order', null=True)),
+ ('published_date', models.DateTimeField(blank=True, null=True)),
+ ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
('difficulty', models.CharField(blank=True, choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advance', 'advance')], default='easy', help_text='Select difficulty', max_length=20, null=True, verbose_name='Difficulty level')),
- ('is_featured', models.BooleanField(default=False, help_text='Is featured')),
- ('description', models.TextField(help_text='Enter answer', null=True)),
- ('thumbnail', models.ImageField(blank=True, null=True, upload_to=bloggy.models.course.upload_thumbnail_image)),
- ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to=settings.AUTH_USER_MODEL)),
- ('category', models.ManyToManyField(blank=True, to='bloggy.category')),
+ ('is_featured', models.BooleanField(default=False, help_text='Should this story be featured on site?')),
+ ('video_id', models.CharField(blank=True, help_text='YouTube Video ID', max_length=100, null=True)),
+ ('github_link', models.CharField(blank=True, help_text='Github project link', max_length=200, null=True)),
+ ('post_type', models.CharField(blank=True, choices=[['post', 'Post'], ['lesson', 'Lesson']], default='article', help_text='Post type', max_length=20, null=True, verbose_name='Post type')),
+ ('template_type', models.CharField(blank=True, choices=[('standard', 'Standard'), ('cover', 'Cover'), ('naked', 'Naked'), ('full', 'Full')], default='standard', help_text='Template type', max_length=20, null=True, verbose_name='Template type')),
+ ('content', models.TextField(help_text='Post content', null=True)),
+ ('thumbnail', models.ImageField(blank=True, null=True, upload_to=bloggy.models.post.upload_thumbnail_image)),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
+ ('category', models.ManyToManyField(to='bloggy.category')),
+ ('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='bloggy.course')),
],
options={
- 'verbose_name': 'course',
- 'verbose_name_plural': 'courses',
- 'ordering': ['-display_order'],
+ 'verbose_name': 'Post',
+ 'verbose_name_plural': 'Posts',
+ 'ordering': ['title'],
},
),
migrations.CreateModel(
@@ -267,7 +336,7 @@ class Migration(migrations.Migration):
('comment_date', models.DateTimeField(auto_now_add=True)),
('active', models.BooleanField(default=False)),
('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reply_set', to='bloggy.comment')),
- ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='bloggy.article')),
+ ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='bloggy.post')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)),
],
options={
@@ -276,33 +345,16 @@ class Migration(migrations.Migration):
'ordering': ['comment_date'],
},
),
- migrations.CreateModel(
- name='Bookmarks',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
- ('post_id', models.IntegerField(help_text='Post id')),
- ('post_type', models.CharField(choices=[('question', 'question'), ('article', 'article')], help_text='Select content type', max_length=20, verbose_name='Content type')),
- ('updated_date', models.DateTimeField(auto_now=True, null=True)),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- ),
- migrations.AddField(
- model_name='article',
- name='category',
- field=models.ManyToManyField(to='bloggy.category'),
+ migrations.AddIndex(
+ model_name='quiz',
+ index=models.Index(fields=['slug', 'publish_status'], name='bloggy_quiz_slug_d36048_idx'),
),
- migrations.AddField(
- model_name='article',
- name='course',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='bloggy.course'),
+ migrations.AddIndex(
+ model_name='post',
+ index=models.Index(fields=['slug', 'publish_status', 'post_type', 'published_date'], name='bloggy_post_slug_560290_idx'),
),
migrations.AddIndex(
model_name='course',
index=models.Index(fields=['slug', 'publish_status', 'published_date'], name='bloggy_cour_slug_7b04ce_idx'),
),
- migrations.AddIndex(
- model_name='article',
- index=models.Index(fields=['slug', 'publish_status', 'post_type', 'published_date'], name='bloggy_arti_slug_0a0597_idx'),
- ),
]
diff --git a/bloggy/migrations/0002_rename_description_category_excerpt.py b/bloggy/migrations/0002_rename_description_category_excerpt.py
new file mode 100644
index 0000000..01b634b
--- /dev/null
+++ b/bloggy/migrations/0002_rename_description_category_excerpt.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.6 on 2023-11-11 16:32
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='category',
+ old_name='description',
+ new_name='excerpt',
+ ),
+ ]
diff --git a/bloggy/migrations/0003_rename_logo_category_thumbnail_page_thumbnail.py b/bloggy/migrations/0003_rename_logo_category_thumbnail_page_thumbnail.py
new file mode 100644
index 0000000..b8d1741
--- /dev/null
+++ b/bloggy/migrations/0003_rename_logo_category_thumbnail_page_thumbnail.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.6 on 2023-11-11 16:48
+
+import bloggy.models.page
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0002_rename_description_category_excerpt'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='category',
+ old_name='logo',
+ new_name='thumbnail',
+ ),
+ migrations.AddField(
+ model_name='page',
+ name='thumbnail',
+ field=models.ImageField(blank=True, null=True, upload_to=bloggy.models.page.image_upload_path),
+ ),
+ ]
diff --git a/bloggy/migrations/0004_remove_redirectrule_is_regx.py b/bloggy/migrations/0004_remove_redirectrule_is_regx.py
new file mode 100644
index 0000000..aaa6c52
--- /dev/null
+++ b/bloggy/migrations/0004_remove_redirectrule_is_regx.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.7 on 2023-11-17 16:54
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0003_rename_logo_category_thumbnail_page_thumbnail'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='redirectrule',
+ name='is_regx',
+ ),
+ ]
diff --git a/bloggy/models/.DS_Store b/bloggy/models/.DS_Store
new file mode 100644
index 0000000..38734ca
Binary files /dev/null and b/bloggy/models/.DS_Store differ
diff --git a/bloggy/models/__init__.py b/bloggy/models/__init__.py
index ec30c72..53a3f97 100644
--- a/bloggy/models/__init__.py
+++ b/bloggy/models/__init__.py
@@ -1,14 +1,12 @@
from .media import Media
from .categories import Category
-from .user import MyUser
-from .post_views import PostViews
-from .post_bookmark import Bookmarks
-from .post_vote import Votes
+from .user import User
+from .post_actions import Bookmark, Vote
from .comment import Comment
-from bloggy.models.course import Course
from .option import Option
-from .post_meta import PostMeta
+from .post import Post
+from .course import Course
+from .quizzes import Quiz, QuizQuestion, QuizAnswer, UserQuizScore
+from .redirect_rule import RedirectRule
from .verification_token import VerificationToken
-from .article import Article
-
diff --git a/bloggy/models/article.py b/bloggy/models/article.py
deleted file mode 100644
index 2d899bd..0000000
--- a/bloggy/models/article.py
+++ /dev/null
@@ -1,147 +0,0 @@
-from django.contrib.contenttypes.fields import GenericRelation
-from django.db import models
-from django.db.models import TextField
-from django.urls import reverse
-from django.utils.html import format_html
-from django.utils.text import slugify
-from hitcount.models import HitCount
-
-import bloggy
-from bloggy import settings
-from bloggy.models import Bookmarks, Category, Votes
-from bloggy.models.course import Course
-from bloggy.models.post import Post
-from bloggy.services.quizz_service import get_questions_json
-from bloggy.utils.string_utils import StringUtils
-
-
-def upload_thumbnail_image(self, post_id):
- return 'uploads/articles/{post_id}'
-
-
-DIFFICULTY_TYPE = [
- ('beginner', 'Beginner'),
- ('intermediate', 'Intermediate'),
- ('advance', 'Advance'),
-]
-
-POST_TYPE = [
- ('article', 'Article'),
- ('quiz', 'Quiz'),
- ('lesson', 'Lesson'),
-]
-
-TEMPLATE_TYPE = [
- ('standard', 'Standard'),
- ('cover', 'Cover'),
- ('naked', 'Naked'),
- ('full', 'Full'),
-]
-
-
-class Article(Post):
- excerpt = models.CharField(max_length=500, help_text='Enter excerpt', null=True, blank=True)
- video_id = models.CharField(max_length=100, help_text='YouTube Video ID', null=True, blank=True)
-
- difficulty = models.CharField(
- max_length=20, choices=DIFFICULTY_TYPE,
- default='beginner', blank=True, null=True,
- help_text="Select difficulty",
- verbose_name="Difficulty level")
-
- post_type = models.CharField(
- max_length=20, choices=POST_TYPE,
- default='article', blank=True, null=True,
- help_text="Post type",
- verbose_name="Post type")
-
- template_type = models.CharField(
- max_length=20, choices=TEMPLATE_TYPE,
- default='standard', blank=True, null=True,
- help_text="Template type",
- verbose_name="Template type")
-
- duration = models.IntegerField(help_text="Duration in minutes. For articles, it will be calculated automatically.",
- default="1")
- is_featured = models.BooleanField(default=False, help_text="Should this story be featured on site?")
- content = TextField(null=True, help_text='Post content')
- thumbnail = models.ImageField(upload_to=upload_thumbnail_image, blank=True, null=True)
-
- # This comes from Django HitCount
- view_count = GenericRelation(HitCount, object_id_field='object_pk', related_query_name='hit_count_generic_relation')
- author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='articles')
- category = models.ManyToManyField(Category)
- course = models.ForeignKey(Course, on_delete=models.PROTECT, blank=True, null=True)
-
- class Meta:
- ordering = ['title']
- verbose_name = "article"
- verbose_name_plural = "articles"
- indexes = [
- models.Index(fields=['slug', 'publish_status', 'post_type', 'published_date']),
- ]
-
- def get_comments_count(self):
- return bloggy.models.Comment.objects.filter(post_id=self.id).count()
-
- def get_admin_url(self):
- return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=[self.id])
-
- def get_absolute_url(self):
- if self.post_type == "quiz":
- return reverse("quiz_single", kwargs={"slug": str(self.slug)})
- elif self.post_type == "lesson":
- return reverse("lesson_single", kwargs={"course": str(self.course.slug), "slug": str(self.slug)})
- else:
- return reverse("article_single", kwargs={"slug": str(self.slug)})
-
- @staticmethod
- def get_excerpt(self):
- return self.excerpt[0, 10]
-
- def get_postmeta(self):
- dictionary = {}
- for postmeta in self.postmeta_set.all():
- dictionary[postmeta.meta_key] = postmeta.meta_value
- return dictionary
-
- @property
- def get_votes_count(self):
- return Votes.objects.all().filter(post_id=self.id).filter(post_type="article").count()
-
- @property
- def get_read_count(self):
- words_per_minute = 150
- word_count = round(len(self.content.split()) / words_per_minute)
- return str(max(word_count, 5)) + " minutes"
-
- def get_bookmarks_count(self):
- return Bookmarks.objects.all().filter(post_id=self.id).count()
-
- def get_questions(self):
- return self.quizquestion_set.all()
-
- def get_lessons(self):
- return self.lesson_set.filter(publish_status="LIVE").order_by("display_order").all()
-
- @property
- def get_questions_json(self):
- return get_questions_json(self)
-
- def thumbnail_tag(self):
- if self.thumbnail:
- return format_html('
'.format(self.thumbnail.url))
-
- thumbnail_tag.short_description = 'Logo'
- thumbnail_tag.allow_tags = True
-
- def save(self, *args, **kwargs):
- if StringUtils.is_blank(self.slug):
- self.slug = slugify(self.title)
- super(Article, self).save(*args, **kwargs)
-
- def __str__(self):
- return self.title
-
- def get_template_path(self):
- return "pages/{}-{}.html".format(self.article, )
diff --git a/bloggy/models/categories.py b/bloggy/models/categories.py
index 45c8120..5ea3204 100644
--- a/bloggy/models/categories.py
+++ b/bloggy/models/categories.py
@@ -1,33 +1,31 @@
from colorfield.fields import ColorField
from django.db import models
from django.urls import reverse
+from django.utils.html import format_html
from django.utils.text import slugify
-from bloggy.models.updatable import Updatable
+from bloggy.models.mixin.SeoAware import SeoAware
+from bloggy.models.mixin.updatable import Updatable
from bloggy.utils.string_utils import StringUtils
-from django.utils.html import format_html
def upload_logo_image(self, filename):
return f'uploads/categories/{filename}'
-PUBLISH_STATUS = [
- ('DRAFT', 'DRAFT'),
- ('LIVE', 'LIVE')
-]
-
-
-class Category(Updatable):
+class Category(Updatable, SeoAware):
title = models.CharField(max_length=150, help_text='Enter title')
article_count = models.IntegerField(default=0)
slug = models.SlugField(max_length=150, help_text='Enter slug', unique=True)
- description = models.TextField(max_length=1000, help_text='Enter description', null=True, blank=True)
- logo = models.ImageField(upload_to=upload_logo_image, null=True)
+ excerpt = models.TextField(max_length=1000, help_text='Enter description', null=True, blank=True)
+ thumbnail = models.ImageField(upload_to=upload_logo_image, null=True)
color = ColorField(default='#1976D2')
publish_status = models.CharField(
- max_length=20, choices=PUBLISH_STATUS,
+ max_length=20, choices=[
+ ('DRAFT', 'DRAFT'),
+ ('LIVE', 'LIVE')
+ ],
default='DRAFT', blank=True, null=True,
help_text="Select publish status",
verbose_name="Publish status")
@@ -40,17 +38,18 @@ class Meta:
def save(self, *args, **kwargs):
if StringUtils.is_blank(self.slug):
self.slug = slugify(self.title)
- super(Category, self).save(*args, **kwargs)
+ super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('categories_single', args=[str(self.slug)])
- def logo_tag(self):
- if self.logo:
- return format_html('
'.format(self.logo.url))
+ def thumbnail_tag(self):
+ if self.thumbnail_tag:
+ return format_html(f'
')
+ return ""
- logo_tag.short_description = 'Logo'
- logo_tag.allow_tags = True
+ thumbnail_tag.short_description = 'Thumbnail'
+ thumbnail_tag.allow_tags = True
def __str__(self):
- return self.title
+ return str(self.title)
diff --git a/bloggy/models/comment.py b/bloggy/models/comment.py
index e63f53e..b390c90 100644
--- a/bloggy/models/comment.py
+++ b/bloggy/models/comment.py
@@ -4,7 +4,7 @@
class Comment(models.Model):
- post = models.ForeignKey('bloggy.Article', on_delete=models.CASCADE, related_name='comments')
+ post = models.ForeignKey('bloggy.Post', on_delete=models.CASCADE, related_name='comments')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments', blank=True,
null=True)
parent = models.ForeignKey('self', related_name='reply_set', null=True, on_delete=models.PROTECT)
diff --git a/bloggy/models/course.py b/bloggy/models/course.py
index 6b33baf..5550186 100644
--- a/bloggy/models/course.py
+++ b/bloggy/models/course.py
@@ -1,23 +1,19 @@
from django.contrib.contenttypes.fields import GenericRelation
-from django.utils.html import format_html
-from django.utils.text import slugify
from django.db import models
+from django.urls import reverse
+from django.utils.html import format_html
from hitcount.models import HitCount
from bloggy import settings
from bloggy.models import Category
-from bloggy.models.post import Post
-from bloggy.utils.string_utils import StringUtils
-from django.urls import reverse
+from bloggy.models.mixin.Content import Content
def upload_thumbnail_image(self, post_id):
return f'uploads/course/{post_id}'
-class Course(Post):
- excerpt = models.CharField(max_length=500, help_text='Enter excerpt', null=True, blank=True)
-
+class Course(Content):
difficulty = models.CharField(
max_length=20, choices=[
('beginner', 'Beginner'),
@@ -28,11 +24,15 @@ class Course(Post):
help_text="Select difficulty",
verbose_name="Difficulty level")
- is_featured = models.BooleanField(default=False, help_text="Is featured")
+ is_featured = models.BooleanField(
+ default=False,
+ help_text="Should this story be featured on site?"
+ )
+
description = models.TextField(null=True, help_text='Enter answer')
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='courses')
thumbnail = models.ImageField(upload_to=upload_thumbnail_image, null=True, blank=True)
- category = models.ManyToManyField(Category, blank=True)
+ category = models.ForeignKey(Category, blank=True, on_delete=models.CASCADE, related_name='courses')
view_count = GenericRelation(HitCount, object_id_field='object_pk', related_query_name='hit_count_generic_relation')
class Meta:
@@ -43,29 +43,17 @@ class Meta:
models.Index(fields=['slug', 'publish_status', 'published_date']),
]
- @staticmethod
- def get_excerpt(self):
- return self.excerpt[0, 10]
-
def get_absolute_url(self):
return reverse("courses_single", kwargs={"slug": str(self.slug)})
@property
def get_lessons(self):
- return self.article_set.filter(post_type="lesson").filter(publish_status="LIVE") \
- .order_by("display_order").all()
+ return self.post_set.filter(publish_status="LIVE").order_by("display_order").all()
def thumbnail_tag(self):
if self.thumbnail:
- return format_html('
'.format(self.thumbnail.url))
+ return format_html(f'
')
+ return ""
thumbnail_tag.short_description = 'Logo'
thumbnail_tag.allow_tags = True
-
- def save(self, *args, **kwargs):
- if StringUtils.is_blank(self.slug):
- self.slug = slugify(self.title)
- super(Course, self).save(*args, **kwargs)
-
- def __str__(self):
- return self.title
diff --git a/bloggy/models/media.py b/bloggy/models/media.py
index 9dc2026..8679aaa 100644
--- a/bloggy/models/media.py
+++ b/bloggy/models/media.py
@@ -16,7 +16,7 @@ class Media(AbstractAttachment):
def save(self, *args, **kwargs):
get_config()['attachment_upload_to'] = f'uploads/{self.post_type}/{self.post_id}'
- super(Media, self).save(*args, **kwargs)
+ super().save(*args, **kwargs)
def get_attachment_upload_to(self):
return f'uploads/{self.post_type}/{self.post_id}'
diff --git a/bloggy/models/mixin/Content.py b/bloggy/models/mixin/Content.py
new file mode 100644
index 0000000..3819874
--- /dev/null
+++ b/bloggy/models/mixin/Content.py
@@ -0,0 +1,42 @@
+from django.db import models
+from django.utils.text import slugify
+from bloggy.models.mixin.SeoAware import SeoAware
+from bloggy.models.mixin.updatable import Updatable
+from bloggy.utils.string_utils import StringUtils
+
+
+class Content(Updatable, SeoAware):
+ title = models.CharField(max_length=300, help_text='Enter title')
+ slug = models.SlugField(max_length=150, help_text='Enter slug', unique=True)
+ excerpt = models.CharField(
+ max_length=500,
+ help_text='Enter excerpt',
+ null=True,
+ blank=True
+ )
+
+ display_order = models.IntegerField(null=True, help_text='Display order', default=0)
+ published_date = models.DateTimeField(null=True, blank=True)
+ publish_status = models.CharField(
+ max_length=20, choices=[
+ ('DRAFT', 'DRAFT'),
+ ('LIVE', 'LIVE'),
+ ('DELETED', 'DELETED')
+ ],
+ default='DRAFT', blank=True, null=True,
+ help_text="Select publish status",
+ verbose_name="Publish status")
+
+ def get_excerpt(self):
+ return self.excerpt[:10]
+
+ def save(self, *args, **kwargs):
+ if StringUtils.is_blank(self.slug):
+ self.slug = slugify(self.title)
+ super().save(*args, **kwargs)
+
+ def __str__(self):
+ return str(self.title)
+
+ class Meta:
+ abstract = True
diff --git a/bloggy/models/mixin/ResizeImageMixin.py b/bloggy/models/mixin/ResizeImageMixin.py
index 9e8bdd3..573864b 100644
--- a/bloggy/models/mixin/ResizeImageMixin.py
+++ b/bloggy/models/mixin/ResizeImageMixin.py
@@ -1,14 +1,16 @@
import uuid
-from PIL import Image
from io import BytesIO
+
+from PIL import Image
from django.core.files import File
from django.core.files.base import ContentFile
from django.db import models
+
# Not used atm
class ResizeImageMixin:
- def resize(self, imageField: models.ImageField, size: tuple):
- im = Image.open(imageField) # Catch original
+ def resize(self, image_field: models.ImageField, size: tuple):
+ im = Image.open(image_field) # Catch original
source_image = im.convert('RGB')
source_image.thumbnail(size) # Resize to size
output = BytesIO()
@@ -19,4 +21,4 @@ def resize(self, imageField: models.ImageField, size: tuple):
file = File(content_file)
random_name = f'{uuid.uuid4()}.jpeg'
- imageField.save(random_name, file, save=False)
+ image_field.save(random_name, file, save=False)
diff --git a/bloggy/models/mixin/SeoAware.py b/bloggy/models/mixin/SeoAware.py
new file mode 100644
index 0000000..81f0072
--- /dev/null
+++ b/bloggy/models/mixin/SeoAware.py
@@ -0,0 +1,10 @@
+from django.db import models
+
+
+class SeoAware(models.Model):
+ meta_title = models.CharField(max_length=120, help_text='Meta Title', blank=True, null=True)
+ meta_description = models.TextField(help_text='Meta Description', blank=True, null=True)
+ meta_keywords = models.CharField(max_length=300, help_text='Meta Keywords', blank=True, null=True)
+
+ class Meta:
+ abstract = True
diff --git a/bloggy/models/updatable.py b/bloggy/models/mixin/updatable.py
similarity index 99%
rename from bloggy/models/updatable.py
rename to bloggy/models/mixin/updatable.py
index d3ea2e9..380d627 100644
--- a/bloggy/models/updatable.py
+++ b/bloggy/models/mixin/updatable.py
@@ -10,5 +10,3 @@ def get_model_type(self):
class Meta:
abstract = True
-
-
diff --git a/bloggy/models/option.py b/bloggy/models/option.py
index 7069b57..eee523c 100644
--- a/bloggy/models/option.py
+++ b/bloggy/models/option.py
@@ -1,7 +1,7 @@
from django.db import models
from django.db.models import TextField
-from bloggy.models.updatable import Updatable
+from bloggy.models.mixin.updatable import Updatable
class Option(Updatable):
@@ -10,4 +10,4 @@ class Option(Updatable):
value = TextField(null=True, help_text='Enter value')
def __str__(self):
- return self.key
+ return str(self.key)
diff --git a/bloggy/models/page.py b/bloggy/models/page.py
new file mode 100644
index 0000000..87143cb
--- /dev/null
+++ b/bloggy/models/page.py
@@ -0,0 +1,44 @@
+from django.db import models
+from django.db.models import TextField
+
+from bloggy.models.mixin.SeoAware import SeoAware
+from bloggy.models.mixin.updatable import Updatable
+
+
+def image_upload_path(self, page_id):
+ return f'uploads/pages/{page_id}'
+
+
+class Page(Updatable, SeoAware):
+ """
+ Stores page data.
+ """
+
+ title = models.CharField(max_length=300, help_text='Enter title')
+ excerpt = models.CharField(
+ max_length=500,
+ help_text='Enter excerpt',
+ null=True,
+ blank=True
+ )
+
+ url = models.CharField(max_length=150, help_text='Enter url', unique=True)
+ thumbnail = models.ImageField(upload_to=image_upload_path, blank=True, null=True)
+ content = TextField(null=True, help_text='Post content')
+ published_date = models.DateTimeField(null=True, blank=True)
+ publish_status = models.CharField(
+ max_length=20, choices=[
+ ('DRAFT', 'DRAFT'),
+ ('LIVE', 'LIVE'),
+ ('DELETED', 'DELETED')
+ ],
+ default='DRAFT', blank=True, null=True,
+ help_text="Select publish status",
+ verbose_name="Publish status")
+
+ def __str__(self):
+ return str(self.title)
+
+ class Meta:
+ verbose_name = 'Page'
+ verbose_name_plural = 'Pages'
diff --git a/bloggy/models/post.py b/bloggy/models/post.py
index 0f313cb..235fdd6 100644
--- a/bloggy/models/post.py
+++ b/bloggy/models/post.py
@@ -1,24 +1,132 @@
+from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
-from bloggy.models.updatable import Updatable
+from django.db.models import TextField
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.text import slugify
+from hitcount.models import HitCount
+import bloggy
+from bloggy import settings
+from bloggy.models import Category, Bookmark
+from bloggy.models.course import Course
+from bloggy.models.mixin.Content import Content
+from bloggy.utils.string_utils import StringUtils
-class Post(Updatable):
- display_order = models.IntegerField(null=True, help_text='Display order', default=0)
- title = models.CharField(max_length=300, help_text='Enter title')
- keywords = models.CharField(max_length=300, help_text='Enter title', null=True, blank=True)
- publish_status = models.CharField(
+def upload_thumbnail_image(self, post_id):
+ return f'uploads/posts/{post_id}'
+
+
+class Post(Content):
+ difficulty = models.CharField(
+ max_length=20, choices=[
+ ('beginner', 'Beginner'),
+ ('intermediate', 'Intermediate'),
+ ('advance', 'advance'),
+ ],
+ default='easy', blank=True, null=True,
+ help_text="Select difficulty",
+ verbose_name="Difficulty level")
+
+ is_featured = models.BooleanField(
+ default=False,
+ help_text="Should this story be featured on site?"
+ )
+
+ video_id = models.CharField(
+ max_length=100,
+ help_text='YouTube Video ID',
+ null=True,
+ blank=True
+ )
+
+ github_link = models.CharField(
+ max_length=200,
+ help_text='Github project link',
+ null=True,
+ blank=True
+ )
+
+ post_type = models.CharField(
+ max_length=20, choices=settings.get_post_types(),
+ default='article', blank=True, null=True,
+ help_text="Post type",
+ verbose_name="Post type")
+
+ template_type = models.CharField(
max_length=20, choices=[
- ('DRAFT', 'DRAFT'),
- ('LIVE', 'LIVE'),
- ('DELETED', 'DELETED')
+ ('standard', 'Standard'),
+ ('cover', 'Cover'),
+ ('naked', 'Naked'),
+ ('full', 'Full'),
],
- default='DRAFT', blank=True, null=True,
- help_text="Select publish status",
- verbose_name="Publish status")
+ default='standard', blank=True, null=True,
+ help_text="Template type",
+ verbose_name="Template type")
+
+ content = TextField(null=True, help_text='Post content')
+ thumbnail = models.ImageField(upload_to=upload_thumbnail_image, blank=True, null=True)
- published_date = models.DateTimeField(null=True, blank=True)
- slug = models.SlugField(max_length=150, help_text='Enter slug', unique=True)
+ # This comes from Django HitCount
+ view_count = GenericRelation(
+ HitCount,
+ object_id_field='object_pk',
+ related_query_name='hit_count_generic_relation'
+ )
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name='posts'
+ )
+ category = models.ManyToManyField(Category)
+ course = models.ForeignKey(Course, on_delete=models.PROTECT, blank=True, null=True)
class Meta:
- abstract = True
+ ordering = ['title']
+ verbose_name = "Post"
+ verbose_name_plural = "Posts"
+ indexes = [
+ models.Index(fields=['slug', 'publish_status', 'post_type', 'published_date']),
+ ]
+
+ def get_comments_count(self):
+ return bloggy.models.Comment.objects.filter(post_id=self.id).count()
+
+ def get_admin_url(self):
+ return reverse(f'admin:{self._meta.app_label}_{self._meta.model_name}_change', args=[self.id])
+
+ def get_absolute_url(self):
+ if self.post_type == "lesson":
+ return reverse("lesson_single", kwargs={"course": str(self.course.slug), "slug": str(self.slug)})
+ else:
+ return reverse("post_single", kwargs={"slug": str(self.slug)})
+
+ @property
+ def get_votes_count(self):
+ return Vote.objects.all().filter(post_id=self.id).filter(post_type="post").count()
+
+ @property
+ def get_read_count(self):
+ words_per_minute = 150
+ word_count = round(len(self.content.split()) / words_per_minute)
+ return str(max(word_count, 5)) + " minutes"
+
+ def get_bookmarks_count(self):
+ return Bookmark.objects.all().filter(post_id=self.id).count()
+
+ def thumbnail_tag(self):
+ if self.thumbnail:
+ return format_html(f'
')
+ return ''
+
+ thumbnail_tag.short_description = 'Logo'
+ thumbnail_tag.allow_tags = True
+
+ def save(self, *args, **kwargs):
+ if StringUtils.is_blank(self.slug):
+ self.slug = slugify(self.title)
+ super().save(*args, **kwargs)
+
+ def get_template_path(self):
+ return f"pages/{self.post_type}-{self.template_type}.html"
diff --git a/bloggy/models/post_actions.py b/bloggy/models/post_actions.py
new file mode 100644
index 0000000..21cc2d9
--- /dev/null
+++ b/bloggy/models/post_actions.py
@@ -0,0 +1,26 @@
+
+from django.db import models
+
+from bloggy import settings
+from bloggy.models.mixin.updatable import Updatable
+
+
+class Vote(Updatable):
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ post_id = models.IntegerField(null=False, help_text='Post id')
+ post_type = models.CharField(
+ null=False,
+ max_length=20,
+ choices=settings.get_post_types(),
+ help_text="Select content type",
+ verbose_name="Content type"
+ )
+
+ class Meta:
+ verbose_name = "Vote"
+ verbose_name_plural = "Votes"
+
+class Bookmark(Vote):
+ class Meta:
+ verbose_name = "Bookmark"
+ verbose_name_plural = "Bookmarks"
diff --git a/bloggy/models/post_bookmark.py b/bloggy/models/post_bookmark.py
deleted file mode 100644
index a4a0545..0000000
--- a/bloggy/models/post_bookmark.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.db import models
-
-from bloggy import settings
-
-
-class Bookmarks(models.Model):
- CONTENT_TYPES = [
- ('question', 'question'),
- ('article', 'article'),
- ]
-
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- post_id = models.IntegerField(null=False, help_text='Post id')
- post_type = models.CharField(null=False, max_length=20, choices=CONTENT_TYPES,
- help_text="Select content type", verbose_name="Content type")
-
- created_date = models.DateTimeField(auto_created=True, null=True, auto_now_add=True)
- updated_date = models.DateTimeField(auto_now=True, null=True)
diff --git a/bloggy/models/post_meta.py b/bloggy/models/post_meta.py
deleted file mode 100644
index a22304c..0000000
--- a/bloggy/models/post_meta.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.db import models
-from django.db.models import TextField
-
-
-class PostMeta(models.Model):
- id = models.AutoField(primary_key=True)
- article = models.ForeignKey('bloggy.Article', on_delete=models.CASCADE)
- meta_key = models.SlugField(max_length=150, help_text='Enter key')
- meta_value = TextField(null=True, help_text='Enter value')
-
- def __str__(self):
- return 'id:{}, meta_key:{}, meta_value:{}'.format(self.id, self.meta_key, self.meta_value)
-
- class Meta:
- verbose_name = "Post metadata"
- verbose_name_plural = "Post metadata"
diff --git a/bloggy/models/post_views.py b/bloggy/models/post_views.py
deleted file mode 100644
index 1ca0570..0000000
--- a/bloggy/models/post_views.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.db import models
-
-
-class PostViews(models.Model):
- CONTENT_TYPES = [
- ('question', 'question'),
- ('article', 'article'),
- ]
-
- ip_address = models.GenericIPAddressField(default="0.0.0.0")
- created_date = models.DateTimeField(auto_created=True, null=True, auto_now_add=True)
- updated_date = models.DateTimeField(auto_now=True, null=True)
-
- def __str__(self):
- return '{0} in {1} post'.format(self.ip_address, self.post.title)
diff --git a/bloggy/models/post_vote.py b/bloggy/models/post_vote.py
deleted file mode 100644
index 6c48c74..0000000
--- a/bloggy/models/post_vote.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.db import models
-
-from bloggy import settings
-
-
-class Votes(models.Model):
- CONTENT_TYPES = [
- ('question', 'question'),
- ('article', 'article'),
- ]
-
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- post_id = models.IntegerField(null=False, help_text='Post id')
- post_type = models.CharField(null=False, max_length=20, choices=CONTENT_TYPES,
- help_text="Select content type", verbose_name="Content type")
-
- created_date = models.DateTimeField(auto_created=True, null=True, auto_now_add=True)
- updated_date = models.DateTimeField(auto_now=True, null=True)
diff --git a/bloggy/models/quizzes.py b/bloggy/models/quizzes.py
index bfb8085..30da1c8 100644
--- a/bloggy/models/quizzes.py
+++ b/bloggy/models/quizzes.py
@@ -1,28 +1,97 @@
+from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import TextField
+from hitcount.models import HitCount
-from bloggy.models import MyUser, Article
-from bloggy.models.updatable import Updatable
+from bloggy import settings
+from bloggy.models import Category
+from bloggy.models.mixin.Content import Content
+from bloggy.models.mixin.updatable import Updatable
+from bloggy.services.quiz_service import get_questions_json
-def upload_logo_image(self, filename):
- return f'uploads/quiz/{filename}'
+def upload_thumbnail_image(self, post_id):
+ return f'uploads/quizzes/{post_id}'
-QUESTION_TYPE = [
- ('binary', 'binary'),
- ('multiple', 'multiple'),
-]
+class Quiz(Content):
+ difficulty = models.CharField(
+ max_length=20,
+ choices=[
+ ('beginner', 'Beginner'),
+ ('intermediate', 'Intermediate'),
+ ('advance', 'advance'),
+ ],
+ default='easy', blank=True, null=True,
+ help_text="Select difficulty",
+ verbose_name="Difficulty level")
+
+ is_featured = models.BooleanField(
+ default=False,
+ help_text="Should this story be featured on site?"
+ )
+ content = TextField(
+ null=True,
+ help_text='Post content'
+ )
+ thumbnail = models.ImageField(
+ upload_to=upload_thumbnail_image,
+ null=True,
+ blank=True)
+ category = models.ForeignKey(
+ Category,
+ blank=True,
+ on_delete=models.CASCADE,
+ related_name='quizzes'
+ )
+ duration = models.IntegerField(
+ help_text="Duration in minutes. For articles, it will be calculated automatically.",
+ default="1"
+ )
+ view_count = GenericRelation(
+ HitCount,
+ object_id_field='object_pk',
+ related_query_name='hit_count_generic_relation'
+ )
+
+ @property
+ def get_questions_json(self):
+ return get_questions_json(self)
+
+ def get_questions(self):
+ return self.quizquestion_set.all()
+
+ class Meta:
+ ordering = ['title']
+ verbose_name = "Quiz"
+ verbose_name_plural = "Quizzes"
+ indexes = [
+ models.Index(fields=['slug', 'publish_status']),
+ ]
class QuizQuestion(models.Model):
id = models.AutoField(primary_key=True)
- title = models.CharField(max_length=500)
- description = TextField(null=True, blank=True, help_text='Enter description')
- explanation = TextField(null=True, blank=True, help_text='Enter explanation', )
- article = models.ForeignKey(Article, on_delete=models.CASCADE, null=True, blank=True)
+ title = models.CharField(
+ max_length=500)
+ description = TextField(
+ null=True, blank=True,
+ help_text='Enter description')
+ explanation = TextField(
+ null=True,
+ blank=True,
+ help_text='Enter explanation', )
+ quiz = models.ForeignKey(
+ Quiz,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True)
type = models.CharField(
- max_length=20, choices=QUESTION_TYPE,
+ max_length=20,
+ choices=[
+ ('binary', 'binary'),
+ ('multiple', 'multiple'),
+ ],
default='binary', blank=True, null=True,
help_text="Select type of question",
verbose_name="Question type")
@@ -45,8 +114,8 @@ def __str__(self):
class UserQuizScore(Updatable):
- quiz = models.ForeignKey(Article, on_delete=models.CASCADE)
- user = models.ForeignKey(MyUser, on_delete=models.CASCADE)
+ quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE)
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
score = models.FloatField()
def __str__(self):
diff --git a/bloggy/models/redirect_rule.py b/bloggy/models/redirect_rule.py
new file mode 100644
index 0000000..102f060
--- /dev/null
+++ b/bloggy/models/redirect_rule.py
@@ -0,0 +1,31 @@
+from django.db import models
+
+from bloggy.models.mixin.updatable import Updatable
+
+
+class RedirectRule(Updatable):
+ source = models.CharField(max_length=300, help_text='Enter from url')
+ destination = models.CharField(max_length=300, help_text='Enter to url')
+ status_code = models.IntegerField(
+ default='standard', blank=True, null=True,
+ choices=[
+ (301, '301 Moved Permanently'),
+ (307, '307 Temporary Redirect'),
+ ],
+ help_text="Redirect type",
+ verbose_name="Redirect type")
+
+ note = models.CharField(
+ max_length=500,
+ help_text='Enter note',
+ null=True,
+ blank=True
+ )
+
+ def __str__(self):
+ return f"{self.status_code}::{self.source}"
+
+
+class Meta:
+ verbose_name = "Redirect"
+ verbose_name_plural = "Redirects"
diff --git a/bloggy/models/subscriber.py b/bloggy/models/subscriber.py
index 0a66b65..e94948e 100644
--- a/bloggy/models/subscriber.py
+++ b/bloggy/models/subscriber.py
@@ -1,6 +1,6 @@
from django.db import models
+
from bloggy import settings
-from bloggy.services import token_generator
from bloggy.services.token_generator import TOKEN_LENGTH
@@ -10,7 +10,6 @@ class Subscribers(models.Model):
confirmed = models.BooleanField(null=True, default=False)
confirmation_code = models.CharField(
default=None,
- # default=token_generator.generate_verification_code(),
max_length=TOKEN_LENGTH,
help_text="The random token identifying the verification request.",
null=True,
@@ -18,8 +17,13 @@ class Subscribers(models.Model):
verbose_name="token",
)
created_date = models.DateTimeField(auto_created=True, null=True, auto_now_add=True)
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='user', blank=True,
- null=True)
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.PROTECT,
+ related_name='user',
+ blank=True,
+ null=True
+ )
def __str__(self):
return self.email + " (" + ("not " if not self.confirmed else "") + "confirmed)"
diff --git a/bloggy/models/user.py b/bloggy/models/user.py
index c847a0f..77133f4 100644
--- a/bloggy/models/user.py
+++ b/bloggy/models/user.py
@@ -1,11 +1,11 @@
-from django.contrib.auth.validators import UnicodeUsernameValidator
-from django.utils import timezone
-
from django.contrib.auth.base_user import AbstractBaseUser
-from django.contrib.auth.models import AbstractUser, PermissionsMixin
+from django.contrib.auth.models import PermissionsMixin
+from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models
from django.urls import reverse
+from django.utils import timezone
from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
import bloggy
from bloggy import settings
@@ -13,7 +13,6 @@
from bloggy.services.account_manager import NewUserAccountManager
from bloggy.services.gravtar import get_gravatar
from bloggy.storage_backends import PublicMediaStorage, StaticStorage
-from django.utils.translation import gettext_lazy as _
def upload_profile_image(self, filename):
@@ -24,7 +23,7 @@ def select_storage():
return PublicMediaStorage() if settings.USE_SPACES else StaticStorage()
-class MyUser(AbstractBaseUser, ResizeImageMixin, PermissionsMixin):
+class User(AbstractBaseUser, ResizeImageMixin, PermissionsMixin):
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_("username"),
@@ -39,11 +38,10 @@ class MyUser(AbstractBaseUser, ResizeImageMixin, PermissionsMixin):
)
email = models.EmailField('email address', unique=True)
- name = models.CharField(_("first name"), max_length=150, blank=True)
+ name = models.CharField(_("full name"), max_length=150, blank=True)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
profile_photo = models.ImageField(null=True, blank=True, upload_to=upload_profile_image,
storage=select_storage) # , storage=PublicMediaStorage()
-
is_staff = models.BooleanField(
"staff status",
default=False,
@@ -70,6 +68,7 @@ class MyUser(AbstractBaseUser, ResizeImageMixin, PermissionsMixin):
bio = models.TextField(max_length=250, null=True, blank=True)
class Meta:
+ db_table = "bloggy_user"
ordering = ['username']
verbose_name_plural = "Users"
@@ -77,24 +76,24 @@ def get_absolute_url(self):
return reverse('user_profile', args=[str(self.username)])
def get_bookmarks_count(self):
- return bloggy.models.Bookmarks.objects.filter(user_id=self.id).count()
+ return bloggy.models.Bookmark.objects.filter(user_id=self.id).count()
def get_full_name_or_username(self):
if self.name:
return self.name
- else:
- return self.username
+
+ return self.username
def get_full_name(self):
- full_name = "%s" % self.name
+ full_name = f"{self.name}"
return full_name.strip()
def get_avatar(self):
if self.profile_photo:
return self.profile_photo.url
- else:
- return get_gravatar(self.email)
+ return get_gravatar(self.email)
def profile_photo_tag(self):
if self.profile_photo:
- return format_html('
'.format(self.profile_photo.url))
+ return format_html(f'
')
+ return ""
diff --git a/bloggy/models/verification_token.py b/bloggy/models/verification_token.py
index b5c7050..198aac8 100644
--- a/bloggy/models/verification_token.py
+++ b/bloggy/models/verification_token.py
@@ -1,8 +1,8 @@
import uuid
+
from django.db import models
from bloggy import settings
-from bloggy.services import token_generator
from bloggy.services.token_generator import TOKEN_LENGTH
TOKEN_TYPE = [
@@ -26,7 +26,6 @@ class VerificationToken(models.Model):
verbose_name="uuid",
)
- # TODO do we need this?
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
help_text="The user who owns the email address.",
@@ -50,7 +49,6 @@ class VerificationToken(models.Model):
token = models.CharField(
help_text="The random token identifying the verification request.",
- # default=token_generator.generate_verification_code(),
max_length=TOKEN_LENGTH,
unique=True,
verbose_name="token",
@@ -66,4 +64,4 @@ def __repr__(self):
return build_repr(
self,
["uuid", "time_created", "code"],
- )
\ No newline at end of file
+ )
diff --git a/bloggy/services/__init__.py b/bloggy/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bloggy/services/email_service.py b/bloggy/services/email_service.py
index 7de33ae..a0a556f 100644
--- a/bloggy/services/email_service.py
+++ b/bloggy/services/email_service.py
@@ -18,7 +18,7 @@ def send_custom_email(subject, recipients, template, args, from_email=settings.D
def send_newsletter_verification_token(request, email, uuid, token):
- subject = "Confirm to StackTips newsletter"
+ subject = f'Confirm to {settings.SITE_TITLE} newsletter'
args = {
"email_subject": subject,
@@ -30,7 +30,7 @@ def send_newsletter_verification_token(request, email, uuid, token):
def email_verification_token(request, new_user, token):
- subject = "{} confirmation code: {}".format(settings.SITE_TITLE, token.code)
+ subject = f"{settings.SITE_TITLE} confirmation code: {token.code}"
args = {
"email_subject": subject,
"verification_code": token.code,
@@ -41,7 +41,7 @@ def email_verification_token(request, new_user, token):
def email_registration_token(request, new_user, verification_token):
- subject = "Welcome to StackTips! Complete your registration and get started!"
+ subject = f'Welcome to {settings.SITE_TITLE}!'
args = {
"email_subject": subject,
"user_name": new_user.name,
diff --git a/bloggy/services/gravtar.py b/bloggy/services/gravtar.py
index c35e8d3..8a27087 100644
--- a/bloggy/services/gravtar.py
+++ b/bloggy/services/gravtar.py
@@ -3,9 +3,6 @@
def get_gravatar(email, size=400):
- default = "identicon" # "https://media.stacktips.com/media/uploads/default_avatar.png"
+ default = "identicon"
params = urllib.parse.urlencode({'d': default, 's': str(size)})
- return "https://www.gravatar.com/avatar/{}?{}".format(
- hashlib.md5(email.lower().encode('utf-8')).hexdigest(),
- params
- )
+ return f"https://www.gravatar.com/avatar/{hashlib.md5(email.lower().encode('utf-8')).hexdigest()}?{params}"
diff --git a/bloggy/services/post_service.py b/bloggy/services/post_service.py
index 2087a62..e3ec89c 100644
--- a/bloggy/services/post_service.py
+++ b/bloggy/services/post_service.py
@@ -1,29 +1,28 @@
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
-from bloggy import models
+from bloggy.models import Post, Quiz
+from bloggy.utils.string_utils import StringUtils
DEFAULT_PAGE_SIZE = 20
def get_recent_feed(publish_status="LIVE", page=1, page_size=DEFAULT_PAGE_SIZE):
- articles = models.Article.objects.prefetch_related("category") \
+ posts = Post.objects.prefetch_related("category") \
.filter(publish_status=publish_status) \
- .filter(post_type__in=["article", "quiz", 'lesson']) \
.order_by("-published_date")
- paginator = Paginator(articles, page_size)
+ paginator = Paginator(posts, page_size)
try:
- articles = paginator.page(page)
+ posts = paginator.page(page)
except PageNotAnInteger:
- articles = paginator.page(1)
+ posts = paginator.page(1)
except EmptyPage:
- articles = paginator.page(paginator.num_pages)
-
- return articles
+ posts = paginator.page(paginator.num_pages)
+ return posts
def get_recent_posts(publish_status="LIVE", page=1, page_size=DEFAULT_PAGE_SIZE):
- articles = models.Article.objects.prefetch_related("category") \
+ articles = Post.objects.prefetch_related("category") \
.filter(publish_status=publish_status).filter(post_type__in=["article"]) \
.order_by("-published_date")
@@ -34,39 +33,34 @@ def get_recent_posts(publish_status="LIVE", page=1, page_size=DEFAULT_PAGE_SIZE)
articles = paginator.page(1)
except EmptyPage:
articles = paginator.page(paginator.num_pages)
-
return articles
def get_recent_quizzes(publish_status="LIVE", page=1):
- articles = models.Article.objects.prefetch_related("category") \
- .filter(publish_status=publish_status).filter(post_type__in=["quiz"]) \
- .order_by("-published_date")
- paginator = Paginator(articles, DEFAULT_PAGE_SIZE)
+ quizzes = Quiz.objects.prefetch_related("category") \
+ .filter(publish_status=publish_status).order_by("-published_date")
+ paginator = Paginator(quizzes, DEFAULT_PAGE_SIZE)
try:
- articles = paginator.page(page)
+ quizzes = paginator.page(page)
except PageNotAnInteger:
- articles = paginator.page(1)
+ quizzes = paginator.page(1)
except EmptyPage:
- articles = paginator.page(paginator.num_pages)
-
- return articles
+ quizzes = paginator.page(paginator.num_pages)
+ return quizzes
-def get_recent_quizzes(publish_status="LIVE", page=1):
- articles = models.Article.objects.filter(publish_status=publish_status, post_type="quiz") \
- .prefetch_related("category", "quizquestion_set") \
- .order_by("-display_order")
- paginator = Paginator(articles, DEFAULT_PAGE_SIZE)
- try:
- articles = paginator.page(page)
- except PageNotAnInteger:
- articles = paginator.page(1)
- except EmptyPage:
- articles = paginator.page(paginator.num_pages)
- return articles
+def set_seo_settings(post, context):
+ if StringUtils.is_blank(post.meta_title):
+ context["meta_title"] = post.title
+ else:
+ context["meta_title"] = post.meta_title
+ if StringUtils.is_blank(post.meta_description):
+ context["meta_description"] = post.excerpt
+ else:
+ context["meta_description"] = post.meta_description
+ context['meta_keywords'] = post.meta_keywords
-def get_quiz_by_id(pk):
- return models.Article.objects.get(pk=pk)
+ if post.thumbnail:
+ context['meta_image'] = post.thumbnail.url
diff --git a/bloggy/services/quizz_service.py b/bloggy/services/quiz_service.py
similarity index 90%
rename from bloggy/services/quizz_service.py
rename to bloggy/services/quiz_service.py
index 0cbb8ea..49df05f 100644
--- a/bloggy/services/quizz_service.py
+++ b/bloggy/services/quiz_service.py
@@ -12,7 +12,6 @@ def get_questions_json(post):
question_answers = question.get_answers()
for index, answer in enumerate(question_answers):
key_char = chr(index + 97)
-
answers.append({
"value": answer.content,
"key": key_char
@@ -30,15 +29,7 @@ def get_questions_json(post):
"correctAnswer": correct_answer
})
- category = post.category.first()
- category_json = None
- if category:
- category_json = {
- "title": category.title,
- "slug": category.slug,
- "id": category.id
- }
-
+ category = post.category
return {
'id': post.id,
'title': post.title,
@@ -49,6 +40,9 @@ def get_questions_json(post):
'logo': post.thumbnail.url if post.thumbnail else "",
'questions': questions,
'time': post.duration,
- 'category': category_json
+ 'category': {
+ "title": category.title,
+ "slug": category.slug,
+ "id": category.id
+ }
}
-
diff --git a/bloggy/services/seo_service.py b/bloggy/services/seo_service.py
index 5e19d79..5d44e93 100644
--- a/bloggy/services/seo_service.py
+++ b/bloggy/services/seo_service.py
@@ -31,18 +31,20 @@ def remove_stopwords(input_string, stopwords):
def get_keywords(content, *extras):
- Soup = BeautifulSoup(content, 'html5lib')
+ soup = BeautifulSoup(content, 'html5lib')
heading_tags = ["h1", "h2", "h3"]
keywords = []
- for tags in Soup.find_all(heading_tags):
+ for tags in soup.find_all(heading_tags):
keywords.append(tags.text.strip())
if extras:
keywords_str = (','.join(extras)) + "," + (','.join(keywords))
else:
keywords_str = ','.join(keywords)
- print("Keywords:%s" % keywords_str)
+ print('Keywords: %s', keywords_str)
filtered_keywords_str = remove_stopwords(keywords_str, stopwords_list)
- print("Keywords:%s" % filtered_keywords_str)
+ print(f"Keywords:{filtered_keywords_str}")
return keywords_str.lower()
+
+
diff --git a/bloggy/services/sitemaps.py b/bloggy/services/sitemaps.py
index 7d33033..e1b5ba0 100644
--- a/bloggy/services/sitemaps.py
+++ b/bloggy/services/sitemaps.py
@@ -1,9 +1,9 @@
from django.contrib import sitemaps
from django.contrib.sitemaps import GenericSitemap
from django.urls import reverse
-
-from ..models import Article, Category, MyUser
+from bloggy.models import Post, Category, User
from bloggy.models.course import Course
+from bloggy.models.page import Page
class StaticPagesSitemap(sitemaps.Sitemap):
@@ -11,39 +11,30 @@ class StaticPagesSitemap(sitemaps.Sitemap):
changefreq = 'daily'
def items(self):
- return [
+ items = []
+ staticPages = [
'index',
'courses',
- 'quizzes',
- 'articles',
+ 'posts',
'categories',
- 'authors',
- 'pages.privacy',
- 'pages.code_of_conduct',
- 'pages.contribute',
- 'pages.about',
- 'pages.contact',
- 'pages.terms_of_service',
- 'pages.cookie_policy',
- 'resources',
- 'resources.status_codes',
- 'resources.base64_converter',
- 'resources.url_encoder',
- 'resources.analyze_http_header',
- 'resources.website_link_analyzer',
- ]
+ 'authors']
- def location(self, item):
- if item.startswith("/"):
- return item
+ for staticPage in staticPages:
+ items.append(reverse(staticPage))
- return reverse(item)
+ pages = Page.objects.filter(publish_status="LIVE").all()
+ for page in pages:
+ items.append(f'/{page.url}')
+ return items
+
+ def location(self, item):
+ return item
sitemaps_list = {
'pages': StaticPagesSitemap,
'articles': GenericSitemap({
- 'queryset': Article.objects.filter(publish_status="LIVE").order_by("-published_date").all(),
+ 'queryset': Post.objects.filter(publish_status="LIVE").order_by("-published_date").all(),
'date_field': 'published_date'
}, priority=0.6, changefreq='daily'),
@@ -57,7 +48,7 @@ def location(self, item):
}, priority=0.6, changefreq='daily'),
'users': GenericSitemap({
- 'queryset': MyUser.objects.exclude(username__in=["siteadmin", "superadmin", "admin"]).filter(is_staff=True).all()
+ 'queryset': User.objects.exclude(username__in=["siteadmin", "superadmin", "admin"]).filter(is_staff=True).all()
}, priority=0.6, changefreq='daily'),
}
diff --git a/bloggy/services/token_generator.py b/bloggy/services/token_generator.py
index 41b298d..f94da1d 100644
--- a/bloggy/services/token_generator.py
+++ b/bloggy/services/token_generator.py
@@ -1,6 +1,3 @@
-import random
-import string
-
import six
from django.contrib.auth.tokens import PasswordResetTokenGenerator
@@ -14,10 +11,3 @@ def _make_hash_value(self, user, timestamp):
six.text_type(user.pk) + six.text_type(timestamp) +
six.text_type(user.is_active)
)
-
-
-def generate_verification_code():
- # Generate a random 20-digit alphanumeric code similar to "JtM8t-MaV8y"
- code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) + '-' + ''.join(
- random.choices(string.ascii_uppercase + string.digits, k=10))
- return code.lower()
diff --git a/bloggy/services/token_service.py b/bloggy/services/token_service.py
index 7d1afe5..347f98a 100644
--- a/bloggy/services/token_service.py
+++ b/bloggy/services/token_service.py
@@ -1,3 +1,5 @@
+import random
+import string
from datetime import timedelta
from django.utils.timezone import now
@@ -19,10 +21,10 @@ def create_token(user, token_type):
# return the existing token, if it is not expired
if time_difference_in_minutes < TOKEN_VALIDITY:
return token
- else:
- token.delete()
- return VerificationToken.objects.create(user=user, token_type=token_type)
+ token.delete()
+
+ return VerificationToken.objects.create(user=user, token_type=token_type, token=generate_verification_code())
def get_token(uuid, verification_token, token_type):
@@ -45,3 +47,10 @@ def is_token_expired(token):
def delete_token_by_uuid(uuid):
return VerificationToken.objects.filter(uuid=uuid).delete()
+
+
+def generate_verification_code():
+ # Generate a random 20-digit alphanumeric code similar to "JtM8t-MaV8y"
+ code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) + '-' + ''.join(
+ random.choices(string.ascii_uppercase + string.digits, k=10))
+ return code.lower()
diff --git a/bloggy/services/url_shortener.py b/bloggy/services/url_shortener.py
index efe5548..3f34a35 100644
--- a/bloggy/services/url_shortener.py
+++ b/bloggy/services/url_shortener.py
@@ -1,9 +1,10 @@
import json
-import os
import logging
+import os
import requests
+from requests import RequestException
logger = logging.getLogger(__name__)
@@ -12,28 +13,28 @@ class UrlShortener:
FIREBASE_API_KEY = os.getenv('FIREBASE_API_KEY')
FIREBASE_DYNAMIC_LINKS_DOMAIN = os.getenv('FIREBASE_DYNAMIC_LINKS_DOMAIN')
- def shorten_url(self, originalLink):
- response_json = self.firebase_api(originalLink)
- logger.debug("Response from Firebase dynamic link", response_json)
+ def shorten_url(self, original_link):
+ response_json = self.firebase_api(original_link)
+ logger.debug("Response from Firebase dynamic link %s", response_json)
if response_json:
response = json.loads(response_json)
return response["shortLink"]
- return originalLink
+ return original_link
- def firebase_api(self, originalLink):
+ def firebase_api(self, original_link):
try:
url = f"https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key={UrlShortener.FIREBASE_API_KEY}"
headers = {'Content-Type': 'application/json'}
payload = json.dumps({
- "longDynamicLink": f"{UrlShortener.FIREBASE_DYNAMIC_LINKS_DOMAIN}?link={originalLink}"
+ "longDynamicLink": f"{UrlShortener.FIREBASE_DYNAMIC_LINKS_DOMAIN}?link={original_link}"
})
response = requests.post(url, data=payload, headers=headers)
if response.status_code == 200:
return response.text
- except Exception:
+ except RequestException:
print("ERROR: while shorting the url")
return None
diff --git a/bloggy/settings.py b/bloggy/settings.py
index 31b7309..900ec43 100644
--- a/bloggy/settings.py
+++ b/bloggy/settings.py
@@ -29,10 +29,6 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DEBUG", "False") == "True"
-
-# enable special cases like tag manager enabled in dev mode. Used to override the default DEBUG behaviour
-DEVELOPMENT_MODE = os.getenv("DEVELOPMENT_MODE", "False") == "True"
-
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1, localhost").split(",")
INTERNAL_IPS = ['127.0.0.1']
@@ -71,7 +67,6 @@
'django.middleware.gzip.GZipMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
- 'bloggy.middleware.wp_redirect.OldUrlRedirectMiddleware', # redirect for old wp site urls
'bloggy.middleware.slash_middleware.AppendOrRemoveSlashMiddleware', # Remove slash from url
# Cache
@@ -88,7 +83,7 @@
# Social login
# 'social_django.middleware.SocialAuthExceptionMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
- 'bloggy.middleware.page_not_found.PageNotFoundMiddleware', # new articles mismatch url redirect
+ 'bloggy.middleware.redirect.RedirectMiddleware', # new articles mismatch url redirect
]
ROOT_URLCONF = 'bloggy.urls'
@@ -180,19 +175,17 @@
STATIC_URL = f'{os.getenv("ASSETS_DOMAIN")}/static/'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
- MEDIA_URL = f'/media/'
+ MEDIA_URL = '/media/'
DEFAULT_FILE_STORAGE = 'bloggy.storage_backends.PublicMediaStorage'
# DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
PRIVATE_MEDIA_LOCATION = 'private'
PRIVATE_FILE_STORAGE = 'bloggy.storage_backends.PrivateMediaStorage'
-
AWS_S3_CUSTOM_DOMAIN = 'media.stacktips.com'
-
else:
STATIC_URL = '/static/'
- STATIC_ROOT = os.path.join(BASE_DIR, 'bloggy/')
+ STATIC_ROOT = os.path.join(BASE_DIR, 'bloggy/static')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
@@ -207,7 +200,7 @@
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
-AUTH_USER_MODEL = 'bloggy.MyUser'
+AUTH_USER_MODEL = 'bloggy.User'
AUTH_USER_DEFAULT_GROUP = 'bloggy-members'
SUMMERNOTE_THEME = 'bs4'
@@ -263,8 +256,8 @@
MESSAGE_STORAGE = "django.contrib.messages.storage.cookie.CookieStorage"
-SITE_TITLE = os.getenv("SITE_TITLE", "Demo Site")
-SITE_TAGLINE = os.getenv("SITE_TAGLINE", "Demo Site")
+SITE_TITLE = os.getenv("SITE_TITLE", "Bloggy")
+SITE_TAGLINE = os.getenv("SITE_TAGLINE", "A perfectly crafted blog that developers love.")
SITE_DESCRIPTION = os.getenv("SITE_DESCRIPTION")
SITE_LOGO = os.getenv("SITE_LOGO")
ASSETS_DOMAIN = os.getenv("ASSETS_DOMAIN")
@@ -289,19 +282,10 @@
CACHE_TTL = 60 * 15
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = CACHE_TTL # number of seconds to cache a page for (TTL)
-CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
+CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same
-if DEBUG:
- CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
- }, 'redis': {
- 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
- 'LOCATION': 'redis://127.0.0.1:6379',
- "KEY_PREFIX": "bloggy"
- },
- }
-else:
+ENABLE_CACHING = os.getenv("ENABLE_CACHING", "False") == "True"
+if ENABLE_CACHING:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
@@ -316,6 +300,12 @@
},
}
}
+else:
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
+ }
+ }
# Django HitCount
HITCOUNT_KEEP_HIT_ACTIVE = {'days': 0}
@@ -326,8 +316,8 @@
SHORTCODES_YOUTUBE_JQUERY = False
# SEO related
-PING_INDEX_NOW_POST_UPDATE = os.getenv("PING_INDEX_NOW_POST_UPDATE", True)
-PING_GOOGLE_POST_UPDATE = os.getenv("PING_GOOGLE_POST_UPDATE", True)
+PING_INDEX_NOW_POST_UPDATE = os.getenv("PING_INDEX_NOW_POST_UPDATE", "True")
+PING_GOOGLE_POST_UPDATE = os.getenv("PING_GOOGLE_POST_UPDATE", "True")
INDEX_NOW_API_KEY = os.getenv("INDEX_NOW_API_KEY", )
# Email configs
@@ -336,9 +326,42 @@
EMAIL_PORT = os.getenv('EMAIL_PORT')
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
-EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', True)
+EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', "True")
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL')
EMAIL_FILE_PATH = os.getenv('EMAIL_FILE_PATH', os.path.join(BASE_DIR, 'test-emails'))
-# ads.txt file content
+# Read the POST_TYPE_CHOICES environment variable from the .env file
+POST_TYPE_CHOICES = os.getenv('POST_TYPE_CHOICES')
+SHOW_EMTPY_CATEGORIES = os.getenv("SHOW_EMTPY_CATEGORIES", "False") == "True"
+
+
+def get_post_types():
+ post_type = [choice.split(':') for choice in POST_TYPE_CHOICES.split(',')]
+ return list(post_type)
+
+
+# enable special cases like tag manager, google ads
+LOAD_GOOGLE_TAG_MANAGER = os.getenv("LOAD_GOOGLE_TAG_MANAGER", "False") == "True"
+LOAD_GOOGLE_ADS = os.getenv("LOAD_GOOGLE_ADS", "False") == "True"
MY_ADS_TXT_CONTENT = os.getenv('MY_ADS_TXT_CONTENT')
+
+LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "handlers": {
+ "console": {
+ "class": "logging.StreamHandler",
+ },
+ },
+ "root": {
+ "handlers": ["console"],
+ "level": "DEBUG",
+ },
+ "loggers": {
+ "django": {
+ "handlers": ["console"],
+ "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"),
+ "propagate": False,
+ },
+ },
+}
\ No newline at end of file
diff --git a/bloggy/shortcodes/parser.py b/bloggy/shortcodes/parser.py
index 6b4cb5d..e98af7a 100644
--- a/bloggy/shortcodes/parser.py
+++ b/bloggy/shortcodes/parser.py
@@ -1,4 +1,5 @@
import re
+
from django.template import Template, Context
@@ -41,7 +42,6 @@ def parse(value):
parsed = re.sub(r'\[' + item + r'\]', result, parsed)
except ImportError:
print("Console error while loading module")
- pass
return parsed
diff --git a/bloggy/signals.py b/bloggy/signals.py
index 8fd533d..7ef0936 100644
--- a/bloggy/signals.py
+++ b/bloggy/signals.py
@@ -1,22 +1,24 @@
+import urllib.parse
from urllib import request
+from urllib.error import URLError
+import django.core
from django.db.models.signals import post_save
from django.dispatch import receiver
+
+import bloggy.models
from bloggy import settings
-from django.core import management
-from bloggy.models import Article
-from urllib.parse import urlencode
PING_GOOGLE_URL = "https://www.google.com/webmasters/tools/ping"
INDEX_NOW = "https://www.bing.com/indexnow?url={}&key={}"
-@receiver(post_save, sender=Article)
+@receiver(post_save, sender=bloggy.models.Post)
def post_saved_action_signal(sender, instance, created, **kwargs):
# Update category count everytime three is a new object added
if created:
- print("Sendr:{}, kwargs:{}".format(sender, kwargs))
- management.call_command('update_category_count')
+ print(f"Sendr:{sender}, kwargs:{kwargs}")
+ django.core.management.call_command('update_category_count')
if instance.publish_status == "PUBLISHED":
if settings.PING_GOOGLE_POST_UPDATE:
@@ -28,19 +30,21 @@ def post_saved_action_signal(sender, instance, created, **kwargs):
def ping_google():
try:
- params = urlencode({"sitemap": settings.SITE_URL + "/sitemap.xml"})
- response = request.urlopen("%s?%s" % (PING_GOOGLE_URL, params))
- if response.code == 200:
- print("Successfully pinged this page for Google!")
- except Exception:
- print("Error while pinging google")
+ sitemap_url = f"{settings.SITE_URL}/sitemap.xml"
+ params = urllib.parse.urlencode({"sitemap": sitemap_url})
+
+ with request.urlopen(f"{PING_GOOGLE_URL}?{params}") as response:
+ if response.code == 200:
+ print("Successfully pinged this page for Google!")
+ except URLError as e:
+ print(f"Error while pinging Google: {e}")
def ping_index_now(article):
try:
- response = request.urlopen(
- INDEX_NOW.format(article.get_absolute_url(), settings.INDEX_NOW_API_KEY))
- if response.code == 200:
- print("Successfully pinged this page for IndexNow!")
- except Exception:
- print("Error while pinging google")
+ url = INDEX_NOW.format(article.get_absolute_url(), settings.INDEX_NOW_API_KEY)
+ with request.urlopen(url) as response:
+ if response.code == 200:
+ print("Successfully pinged this page for IndexNow!")
+ except URLError as e:
+ print(f"Error while pinging IndexNow: {e}")
diff --git a/bloggy/storage_backends.py b/bloggy/storage_backends.py
index b123eaa..c6897b1 100644
--- a/bloggy/storage_backends.py
+++ b/bloggy/storage_backends.py
@@ -8,6 +8,7 @@ class StaticStorage(FileSystemStorage):
class PublicMediaStorage(S3Boto3Storage):
+
location = 'media'
default_acl = 'public-read'
file_overwrite = False
diff --git a/bloggy/templates/220764bdee4b4ff297c588217aaaafa3.txt b/bloggy/templates/220764bdee4b4ff297c588217aaaafa3.txt
deleted file mode 100644
index dd75559..0000000
--- a/bloggy/templates/220764bdee4b4ff297c588217aaaafa3.txt
+++ /dev/null
@@ -1 +0,0 @@
-220764bdee4b4ff297c588217aaaafa3
\ No newline at end of file
diff --git a/bloggy/templates/admin/base_site.html b/bloggy/templates/admin/base_site.html
index ec04a68..adfd9f4 100644
--- a/bloggy/templates/admin/base_site.html
+++ b/bloggy/templates/admin/base_site.html
@@ -32,7 +32,7 @@