├── .prettierrc ├── gmt ├── views │ ├── __init__.py │ ├── api.py │ ├── articles.py │ ├── admin.py │ └── general.py ├── templates │ ├── general │ │ ├── robots.txt │ │ ├── sitemap.xml │ │ ├── contribute.html │ │ ├── privacy_policy.html │ │ ├── tos.html │ │ ├── credits.html │ │ ├── morning.html │ │ └── contact.html │ ├── admin │ │ └── index.html │ ├── auth │ │ ├── success.html │ │ ├── unsubscribe.html │ │ └── confirm.html │ ├── 404.html │ ├── writers │ │ ├── login.html │ │ ├── guidelines.html │ │ ├── portal.html │ │ ├── settings.html │ │ ├── apply.html │ │ └── register.html │ └── articles │ │ └── article.html ├── static │ ├── Meta-Images │ │ └── Landing Card.png │ ├── JavaScript │ │ ├── landing-page │ │ │ ├── scroller.js │ │ │ └── typewriter.js │ │ ├── writers │ │ │ ├── filter.js │ │ │ ├── create-articles.js │ │ │ ├── user-apply.js │ │ │ └── create-profile.js │ │ ├── layout │ │ │ ├── navbar.js │ │ │ └── darkmode.js │ │ └── signup │ │ │ └── signup.js │ ├── loader.js │ ├── config.css │ └── loader.css ├── news.py ├── utils.py ├── __init__.py └── extras.py ├── .vercelignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── lint.yaml │ ├── black.yml │ ├── cron.yaml │ ├── articles.yaml │ └── codeql.yml └── dependabot.yml ├── pyproject.toml ├── requirements-dev.txt ├── nginx ├── Dockerfile └── nginx.conf ├── index.py ├── .gitignore ├── vercel.json ├── .pre-commit-config.yaml ├── docker-compose.yml ├── requirements.txt ├── rss.json ├── Dockerfile ├── LICENSE ├── config.template.py ├── package.json ├── tailwind.config.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /gmt/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | instance/ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: goodmorningtech 2 | -------------------------------------------------------------------------------- /gmt/templates/general/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==24.4.2 2 | djlint==1.36.4 3 | pre-commit==4.2.0 4 | isort==5.13.2 5 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | COPY nginx.conf /etc/nginx/conf.d -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from gmt import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run() 7 | -------------------------------------------------------------------------------- /gmt/static/Meta-Images/Landing Card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodMorninTech/GoodMorningTech/HEAD/gmt/static/Meta-Images/Landing Card.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | *.pyc 4 | __pycache__/ 5 | 6 | instance/ 7 | 8 | dist/ 9 | build/ 10 | *.egg-info/ 11 | 12 | .idea/ 13 | 14 | node_modules/ 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | about: Join our discord server to ask questions 5 | url: https://discord.goodmorningtech.news -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "index.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: psf/black@stable 11 | with: 12 | options: "--line-length 101" 13 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream gmt { 2 | server web:5000; 3 | } 4 | 5 | server { 6 | 7 | listen 80; 8 | 9 | location / { 10 | proxy_pass http://gmt; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header Host $host; 13 | proxy_redirect off; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /gmt/static/JavaScript/landing-page/scroller.js: -------------------------------------------------------------------------------- 1 | const scroller = document.getElementById('scroller'); 2 | scroller.addEventListener('click', () => { 3 | window.scrollTo({ 4 | top: document.getElementById('what-do-we-offer').offsetTop, 5 | behavior: 'smooth' 6 | }); 7 | }); 8 | 9 | scroller.addEventListener('mouseover', () => { 10 | scroller.style.cursor = 'pointer'; 11 | }); -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - repo: https://github.com/psf/black 9 | rev: 22.12.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/pycqa/isort 13 | rev: 5.11.4 14 | hooks: 15 | - id: isort 16 | -------------------------------------------------------------------------------- /gmt/static/loader.js: -------------------------------------------------------------------------------- 1 | const loader = document.querySelector('#loader-wrapper'); 2 | const isReduced = window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; 3 | 4 | if (!isReduced) { 5 | setTimeout(() => { 6 | loader.style.opacity = 0; 7 | loader.style.display = 'none'; 8 | }, 3000); 9 | } else { 10 | loader.style.display = 'none'; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: . 4 | container_name: goodmorningtech_web 5 | restart: unless-stopped 6 | environment: 7 | - FLASK_APP=index.py 8 | - FLASK_ENV=production 9 | volumes: 10 | - .:/app 11 | expose: 12 | - 5000 13 | 14 | nginx: 15 | build: ./nginx 16 | container_name: goodmorningtech_nginx 17 | restart: unless-stopped 18 | ports: 19 | - 5000:80 20 | depends_on: 21 | - web 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask-Mail==0.10.0 2 | Flask-PyMongo==2.3.0 3 | Flask-WTF==1.2.1 4 | Flask==2.3.2 5 | WTForms==3.2.1 6 | beautifulsoup4==4.12.3 7 | email-validator==2.1.1 8 | feedparser==6.0.11 9 | itsdangerous==2.2.0 10 | markdown==3.5 11 | requests==2.32.3 12 | Flask-Session2==1.3.1 13 | pymongo==4.8.0 14 | Flask-mde==1.2.1 15 | Flask-login==0.6.3 16 | arrow==1.3.0 17 | pytz==2022.7.1 18 | lxml==5.3.1 19 | Flask-Admin==1.6.1 20 | flask-crontab==0.1.2 21 | gunicorn==23.0.0 22 | flask-turnstile==0.1.1 23 | mistralai==1.5.1 -------------------------------------------------------------------------------- /gmt/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% block body %} 3 | {{ super() }} 4 |
41 | Return Home
43 | 44 | Also check out: 45 | 47 | 48 | Morning the Bot 49 | 50 |
51 |8 | We take your privacy seriously. Please read this Privacy Policy to learn more about how we collect, use and protect your personal information. 9 |
10 |We may collect the following types of personal information:
12 |We use this information to send you news.
17 |18 | We will not sell, distribute or lease your personal information to third parties unless we have your permission or are required by law to do so. 19 |
20 |22 | We are committed to ensuring that your information is secure. In order to prevent unauthorized access or disclosure, we have put in place suitable physical, electronic and managerial procedures to safeguard and secure the information we collect online. 23 |
24 |26 | We use cookies to keep the website running, like saving your log in. A cookie is a small text file that a website saves on your computer or mobile device when you visit the site. You can control cookies through your browser settings. 27 |
28 |30 | Our website may contain links to other websites of interest. However, once you have used these links to leave our site, you should note that we do not have any control over that other website. Therefore, we cannot be responsible for the protection and privacy of any information which you provide while visiting such sites and such sites are not governed by this Privacy Policy. 31 |
32 |34 | We reserve the right to update or change this Privacy Policy at any time. If we make material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our website. 35 |
36 |37 | If you have any questions or concerns about this Privacy Policy, please contact us at support@goodmorningtech.news. 38 |
39 |{{ error }}
{% endif %} 35 | 50 |8 | By using our website, you agree to comply with and be bound by the following terms and conditions of use: 9 |
10 |12 | These Terms and Conditions ("Agreement") are between you ("User" or "you") and our company ("Company", "we", or "us"). This Agreement sets forth the general terms and conditions of your use of the website and any of its products or services. 13 |
14 |23 | The website and its products are provided “as is” without warranty of any kind, either express or implied, including without limitation warranties of merchantability, fitness for a particular purpose, or non-infringement. 24 |
25 |27 | In no event shall we or our suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on our website, even if we or an authorized representative has been notified orally or in writing of the possibility of such damage. 28 |
29 |31 | You agree to indemnify and hold us harmless from any liability, loss, claim, and expense (including reasonable attorney's fees) related to your violation of this Agreement or use of the website and any of its products or services. 32 |
33 |35 | This Agreement shall be governed by and construed in accordance with the laws of the State of California, United States, without giving effect to any principles of conflicts of law. 36 |
37 |39 | We reserve the right, at our sole discretion, to modify or replace these Terms and Conditions at any time. If a revision is material, we will provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion. 40 |
41 |If you have any questions about these Terms of Service, please contact us at support@goodmorningtech.news.
42 |{{ status }}
17 |46 | If you haven't applied: Apply Here 47 |
48 |49 | If you got accepted and don't have an account: Register Here. 50 |
51 |8 | These guidelines outline the expectations for writers creating content on behalf of Good Morning Tech for use in our newsletters. These guidelines apply to any writer who creates content on behalf of the Company, including employees and independent contractors. 9 |
10 |12 | Good Morning Tech values original research and insights. All writers must strive to deliver unique information, whether through exclusive data or interviews with industry experts. Plagiarism is strictly prohibited. 13 |
14 |16 | Our newsletters are personal in nature. Therefore, writers should maintain a conversational tone that is engaging and easy to read. Technical language and jargon should be avoided whenever possible. All content should be written in a manner that is sensitive to our readers. 17 |
18 |20 | While Good Morning Tech values unique insights, it is also critical to provide supporting evidence for such insights. This includes links to relevant studies, reports or news articles. Writers should always respect copyright laws and fair use guidelines when referencing other sources. 21 |
22 |24 | Our readers trust us to provide accurate and balanced information. Therefore, writers must remain objective and unbiased in their writing. If an article has a strong point of view, it must be clearly labeled as an opinion piece. The Company reserves the right to reject content which breaches its standards of impartiality. 25 |
26 |28 | If a writer references another source in their work, it is essential that they provide clear attribution. This includes proper citation and providing a link to the original article wherever possible. The Company retains the right to verify all references made in any submitted content. 29 |
30 |32 | Accuracy is paramount when it comes to writing for our newsletters. Therefore, all writers must thoroughly proofread their work and fact-check any information before submitting it. The Company reserves the right to verify all facts presented in any submitted content. 33 |
34 |36 | Writers must be open to receiving feedback from editors and other team members. This will help to ensure that the final product is the best it can be. All critique and feedback will be delivered in a respectful and constructive manner. 37 |
38 |40 | Good Morning Tech values originality, accuracy, objectivity, and professionalism in all content created for our newsletters. By adhering to these guidelines, writers will be contributing towards maintaining standards and quality of our newsletters. 41 |
42 |44 | All writers must accept these guidelines before creating their first article. By submitting content or accepting payment, writers acknowledge their acceptance of these guidelines. 45 |
46 | tag
92 | text = text.replace(
93 | "",
94 | '',
95 | )
96 | text = text.replace("", "")
97 | return text
98 |
99 |
100 | def random_language_greeting():
101 | json = {
102 | "english": "Good Morning",
103 | "spanish": "¡Buenos días",
104 | "chinese": "早上好!",
105 | "hindi": "शुभ प्रभात",
106 | "arabic": "صباح الخير",
107 | "portuguese": "Bom dia",
108 | "bengali": "শুভ সকাল",
109 | "russian": "Доброе утро",
110 | "japanese": "おはようございます!",
111 | "punjabi": "ਸ਼ੁਭ ਸਵੇਰ",
112 | "german": "Guten Morgen",
113 | "georgian": "დილა მშვიდობის",
114 | "korean": "안녕하세요",
115 | "french": "Bonjour",
116 | "turkish": "Günaydın",
117 | "italian": "Buongiorno",
118 | "urdu": "صبح بخیر",
119 | "polish": "Dzień dobry",
120 | "javanese": "Selamat pagi",
121 | "marathi": "शुभ प्रभात",
122 | "dutch": "Goedemorgen",
123 | }
124 | language, value = random.choice(list(json.items()))
125 | return language.capitalize(), value
126 |
--------------------------------------------------------------------------------
/gmt/views/articles.py:
--------------------------------------------------------------------------------
1 | from bson import ObjectId
2 | import markdown
3 | from flask import (
4 | Blueprint,
5 | abort,
6 | redirect,
7 | render_template,
8 | request,
9 | session,
10 | url_for,
11 | current_app,
12 | )
13 | from datetime import datetime
14 |
15 | from flask_login import login_required, current_user
16 |
17 | from .. import mongo
18 | from ..utils import clean_html, upload_file
19 |
20 | bp = Blueprint("articles", __name__, url_prefix="/articles")
21 |
22 |
23 | @bp.route("/", methods=("POST", "GET"))
24 | def article(article_id):
25 | if current_user.is_authenticated:
26 | current_user.writer = mongo.db.writers.find_one(
27 | {"_id": ObjectId(current_user.id)}
28 | )
29 |
30 | article_db = mongo.db.articles.find_one({"_id": ObjectId(article_id)})
31 | # if article doesnt exists 404
32 | if not article_db:
33 | return render_template("404.html")
34 |
35 | if request.method == "POST":
36 | # DELETES ARTICLE
37 | if current_user.is_authenticated:
38 | mongo.db.articles.delete_one({"_id": ObjectId(article_id)})
39 | return redirect(url_for("writers.portal"))
40 |
41 | content_md = markdown.markdown(article_db["content"])
42 |
43 | date = article_db["date"].strftime("%d %B %Y")
44 |
45 | article_db["views"] = int(article_db["views"]) + 1
46 | mongo.db.articles.update_one(
47 | {"_id": ObjectId(article_id)}, {"$set": {"views": article_db["views"]}}
48 | )
49 |
50 | return render_template(
51 | "articles/article.html",
52 | article=article_db,
53 | content=content_md,
54 | date=date,
55 | no_meta=True,
56 | )
57 |
58 |
59 | @bp.route("/edit/", methods=("POST", "GET"))
60 | @login_required
61 | def edit(article_id):
62 | current_user.writer = mongo.db.writers.find_one({"_id": ObjectId(current_user.id)})
63 |
64 | article_db = mongo.db.articles.find_one({"_id": ObjectId(article_id)})
65 | if not article_db:
66 | return render_template("404.html")
67 | if article_db["author"]["email"] != current_user.writer["email"]:
68 | return abort(403)
69 |
70 | if request.method == "POST":
71 | title = request.form.get("title")
72 | if not title:
73 | return render_template(
74 | "writers/create.html",
75 | status=f"Please enter a title!",
76 | article=article_db,
77 | )
78 | description = request.form.get("description")
79 | if not description:
80 | return render_template(
81 | "writers/create.html",
82 | status=f"Please enter a description!",
83 | article=article_db,
84 | )
85 | content = request.form.get("content")
86 | if not content:
87 | return render_template(
88 | "writers/create.html",
89 | status=f"Please enter some content!",
90 | article=article_db,
91 | )
92 | thumbnail = request.files.get("thumbnail", None)
93 | categories = request.form.getlist("category")
94 |
95 | if not categories:
96 | return render_template(
97 | "writers/create.html",
98 | status=f"Please select atleast one category!",
99 | article=article_db,
100 | )
101 |
102 | if thumbnail:
103 | if not upload_file(
104 | file=thumbnail, filename=article_db["_id"], current_app=current_app
105 | ):
106 | return render_template(
107 | "writers/create.html",
108 | status=f"Error uploading thumbnail! Uploaded without thumbnail,"
109 | f" edit article to add one!",
110 | article=article_db,
111 | )
112 |
113 | mongo.db.articles.update_one(
114 | {"_id": ObjectId(article_id)},
115 | {
116 | "$set": {
117 | "title": title,
118 | "description": description,
119 | "content": clean_html(content),
120 | "categories": categories,
121 | }
122 | },
123 | )
124 | return redirect(url_for("articles.article", article_id=article_id))
125 |
126 | return render_template("articles/edit.html", article=article_db)
127 |
--------------------------------------------------------------------------------
/gmt/templates/general/credits.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Credits and Tech{% endblock %}
3 | {% block body %}
4 |
5 | How it's made: Good Morning Tech
6 |
7 | Designed with
8 |
9 | Figma, Adobe Photoshop and Dribbble
10 |
11 | Built with
12 |
13 | Python for the backend. HTML5, CSS3 and Javascript for the frontend
14 |
15 | Tech Stack
16 |
17 |
18 | For the backend we used Flask together with MongoDB for the Database. For the
19 | frontend we used Jinja Templates and Tailwind. For the website hosting we used Vercel and for the emails and
20 | Images we used Cybrancee.
21 |
22 | For a more detailed technical breakdown, check out Levani's article about it. (not yet written)
23 |
24 |
25 | Developed by
26 |
27 |
28 | Good Morning Tech is developed by:
29 |
30 | @HarryDaDev(Design
31 | and Frontend)
32 |
33 | @OpenSourceSimon(Backend)
34 |
35 | @LevaniVashadze(Backend
36 | and Frontend)
37 |
38 | @Kappa(Backend)
39 |
40 | More Contributors are on the GitHub page, and we are always looking for more contributors.
41 |
42 |
43 | Additionally
44 |
45 |
46 | Special thanks to Lewis Menelaws(Coding with Lewis) for having faith in the project and giving us
47 | the goodmorningtech.news domain.
48 |
49 |
50 | Disclaimer news content
51 |
52 |
53 | We would like to take a moment to clarify that not all of the news content on
54 | our website is our original work. As a news aggregator, we strive to provide our readers with the latest and
55 | most accurate news from various reliable sources.
56 | While we do publish some original content, a significant portion of the news articles you will find on our
57 | website are sourced from other news outlets. We carefully select and curate the news stories that we believe are
58 | most relevant and informative to our readers, and we always provide proper attribution to the original sources.
59 | We encourage our readers to visit the original sources of the news articles we publish to gain a more
60 | comprehensive understanding of the topics covered. We also welcome any feedback or suggestions on how we can
61 | improve our news coverage and better serve our readers.
62 | Thank you for your continued support and trust in our website as a source of news and information.
63 |
64 |
65 | {% endblock %}
66 |
--------------------------------------------------------------------------------
/gmt/static/JavaScript/writers/user-apply.js:
--------------------------------------------------------------------------------
1 | let totalSteps = document.getElementById('totalSteps');
2 | let currentStep = document.getElementById('currentStep');
3 |
4 | let email = document.getElementById('userEmail');
5 | let contact = document.getElementById('userContact');
6 | let displayName = document.getElementById('username');
7 |
8 | let stepOne = document.getElementById('stepOne');
9 | let stepOneBtn = document.getElementById('nextBtnStepOne');
10 |
11 | const progressBar = document.getElementById('progressBar');
12 |
13 | stepOneBtn.addEventListener('click', function () {
14 | if (email.value === '' || contact.value === '' || displayName.value === '') {
15 | if (email.value === '') {
16 | email.classList.add('border-red-500', 'border-2');
17 | }
18 | if (contact.value === '') {
19 | contact.classList.add('border-red-500', 'border-2');
20 | }
21 | if (displayName.value === '') {
22 | displayName.classList.add('border-red-500', 'border-2');
23 | }
24 | } else {
25 | stepOne.classList.add('hidden');
26 | stepTwo.classList.remove('hidden');
27 | currentStep.textContent = 2;
28 | progressBar.classList.remove('w-1/3');
29 | progressBar.classList.add('w-2/3');
30 | }
31 | });
32 |
33 | email.addEventListener('blur', function () {
34 | email.classList.remove('border-red-500', 'border-2');
35 | });
36 | contact.addEventListener('blur', function () {
37 | contact.classList.remove('border-red-500', 'border-2');
38 | });
39 | displayName.addEventListener('blur', function () {
40 | displayName.classList.remove('border-red-500', 'border-2');
41 | });
42 |
43 | const checkEmail = () => {
44 | if (email.value.includes('@') && email.value.includes('.')) {
45 | email.classList.remove('border-red-500', 'border-2');
46 | email.classList.add('border-green-500', 'border-2');
47 | } else {
48 | email.classList.remove('border-green-500', 'border-2');
49 | email.classList.add('border-red-500', 'border-2');
50 | }
51 | }
52 | email.addEventListener('click', checkEmail);
53 | email.addEventListener('input', checkEmail);
54 | email.addEventListener('blur', checkEmail);
55 |
56 | let goBackBtnStepTwo = document.getElementById('backBtnStepTwo');
57 | let stepTwoBtn = document.getElementById('nextBtnStepTwo');
58 | let stepTwo = document.getElementById('stepTwo');
59 |
60 | let whyWrite = document.getElementById('whyWrite');
61 | let topics = document.getElementById('topics');
62 |
63 | goBackBtnStepTwo.addEventListener('click', function () {
64 | stepTwo.classList.add('hidden');
65 | stepOne.classList.remove('hidden');
66 | currentStep.textContent = 1;
67 | progressBar.classList.remove('w-2/3');
68 | progressBar.classList.add('w-1/3');
69 | });
70 |
71 | stepTwoBtn.addEventListener('click', function () {
72 | if (whyWrite.value === '' || topics.value === '') {
73 | if (whyWrite.value === '') {
74 | whyWrite.classList.add('border-red-500', 'border-2');
75 | }
76 | if (topics.value === '') {
77 | topics.classList.add('border-red-500', 'border-2');
78 | }
79 | } else {
80 | stepTwo.classList.add('hidden');
81 | stepThree.classList.remove('hidden');
82 | currentStep.textContent = 3;
83 | progressBar.classList.remove('w-2/3');
84 | progressBar.classList.add('w-full');
85 | }
86 | }
87 | );
88 |
89 | whyWrite.addEventListener('blur', function () {
90 | whyWrite.classList.remove('border-red-500', 'border-2');
91 | });
92 |
93 | topics.addEventListener('blur', function () {
94 | topics.classList.remove('border-red-500', 'border-2');
95 | });
96 |
97 | let goBackBtnStepThree = document.getElementById('backBtnStepThree');
98 | let stepThree = document.getElementById('stepThree');
99 |
100 | goBackBtnStepThree.addEventListener('click', function () {
101 | stepThree.classList.add('hidden');
102 | stepTwo.classList.remove('hidden');
103 | currentStep.textContent = 2;
104 | progressBar.classList.remove('w-full');
105 | progressBar.classList.add('w-2/3');
106 | });
107 |
108 | let submitFormBtn = document.getElementById('submitFormBtn');
109 | let sample = document.getElementById('sample');
110 | let article = document.getElementById('article');
111 | let checkbox = document.getElementById('terms');
112 |
113 | const checkButton = () => {
114 | submitFormBtn.disabled = article.value === '' || checkbox.checked === false;
115 | }
116 |
117 | document.addEventListener('DOMContentLoaded', checkButton);
118 | article.addEventListener('input', checkButton);
119 | checkbox.addEventListener('click', checkButton);
120 |
121 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to GoodMorningTech 💻📝
2 |
3 | Thank you for your interest in contributing to GoodMorningTech! 🙌 Whether you're a programmer 💻, writer 📝, or community helper 🤝, your contributions are greatly appreciated.
4 |
5 | ## Table of Contents
6 | 1. [How to Contribute](#how-to-contribute-)
7 | 2. [Code of Conduct](#code-of-conduct-)
8 | 3. [Getting Started](#getting-started-)
9 | 4. [Bug Reporting](#bug-reporting-)
10 | 5. [Enhancement Requests](#enhancement-requests-)
11 | 6. [Contact Information](#contact-information-)
12 |
13 | ## How to Contribute 🤔
14 |
15 | Here are a few ways you can contribute to the GoodMorningTech project:
16 |
17 | - **Programming**: If you have programming experience and would like to contribute to the code base, check out the [open issues](https://github.com/GoodMorninTech/GoodMorningTech/issues) to see what needs to be done or create an issue or pull request, and we will review it. Before you start working on a new feature or bug fix, make sure to fork the project and work on the development branch and open a pull request when you're ready to submit your changes. 💻
18 |
19 | - **Writing**: If you have a passion for writing and would like to contribute to our news, Apply at this link [Application Link](https://goodmorningtech.news/writers/apply), if we accept you we will reach out. 📝
20 |
21 | - **Community Help**: If you'd like to help out the community in any way, let us know, or join our [discord](https://discord.goodmorningtech.news)! We're always looking for ways to improve and make a positive impact. 🤝
22 |
23 | ## Code of Conduct 📜
24 |
25 | To ensure that the GoodMorningTech community is welcoming and inclusive to all, we have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors must follow. Please take a moment to read it before contributing.
26 |
27 | ## Getting Started 🚀
28 |
29 | To get started fork the Repository on GitHub.
30 |
31 | ### Prerequisites
32 | - Python
33 | - Node.js
34 |
35 | If you don't have these just install them from the official websites.
36 |
37 | #### Cloning the repository
38 | Clone the forked repository(make sure to insert your username):
39 | ```
40 | git clone https://github.com/YOURGITHUBUSERNAME/GoodMorningTech.git
41 | ```
42 | Move into the new directory:
43 | ```
44 | cd GoodMorningTech
45 | ```
46 | #### Configuration
47 | Create an `instance` folder:
48 | ```
49 | mkdir instance
50 | ```
51 | Move the configuration template into `instance` and rename it to `config.py`:
52 | Windows:
53 | ```
54 | copy config.template.py config.py
55 | move config.py instance
56 | ```
57 | Linux:
58 | ```
59 | cp config.template.py config.py
60 | mkdir instance
61 | mv config.py instance/config.py
62 | ```
63 | Edit the configuration file and set the fields to your liking.
64 |
65 | For the database to work create a mongoDB database and set the `MONGO_URI` to your database.
66 |
67 | To use the email functionality set the `MAIL_USERNAME`,`MAIL_PASSWORD` and `MAIL_SERVER`
68 | to your email credentials, for gmail you might need to configure extra stuff,
69 | look up recent guides on how to use SMTP with gmail.
70 |
71 | Alternatively you can configure everything from environment variables, make sure to set all the variables in `config.template.py`.
72 |
73 | #### Set Up for Development
74 | Install the development requirements:
75 | ```
76 | pip install -r requirements-dev.txt
77 | ```
78 | ```
79 | npm install
80 | ```
81 |
82 | [//]: # (#### Install pre-commit hooks:)
83 |
84 | [//]: # (```)
85 |
86 | [//]: # (pre-commit install)
87 |
88 | [//]: # (```)
89 |
90 | #### Running the Server
91 | Install the requirements:
92 | ```
93 | pip install -r requirements.txt
94 | ```
95 | Run the application:
96 | ```
97 | python -m flask --app gmt --debug run
98 | ```
99 | and in separate terminal run the tailwind compiler:
100 | ```
101 | npm run tailwind
102 | ```
103 |
104 | ## Bug Reporting 🐛
105 |
106 | If you find a bug in the project, we would love to know about it! Before reporting a bug, please check the [open issues](https://github.com/GoodMorninTech/GoodMorningTech/issues) to see if it has already been reported. If not, please create a new issue and provide as much detail as possible about the bug, including steps to reproduce it.
107 |
108 | ## Enhancement Requests 💡
109 |
110 | If you have an idea for a new feature or enhancement, we'd love to hear about it! Before creating a new enhancement request, please check the [open issues](https://github.com/GoodMorninTech/GoodMorningTech/issues) to see if it has already been suggested. If not, please create a new issue and provide as much detail as possible about the enhancement.
111 |
112 | ## Contact Information 📞
113 |
114 | If you have any questions or would like to get in touch with us, feel free to send us an email at [support@goodmorningtech.news](mailto:support@goodmorningtech.news) or join our Discord server at [discord.goodmorningtech.news](https://discord.goodmorningtech.news/). We're always here to help and support you in your contributions! 🤗
115 |
--------------------------------------------------------------------------------
/gmt/static/JavaScript/writers/create-profile.js:
--------------------------------------------------------------------------------
1 | let totalSteps = document.getElementById('totalSteps');
2 | let currentStep = document.getElementById('currentStep');
3 |
4 | let email = document.getElementById('userEmail');
5 | let name = document.getElementById('username');
6 | let displayName = document.getElementById('displayName');
7 |
8 | let stepOne = document.getElementById('stepOne');
9 | let stepOneBtn = document.getElementById('nextBtnStepOne');
10 |
11 | const progressBar = document.getElementById('progressBar');
12 |
13 | stepOneBtn.addEventListener('click', function () {
14 | if (email.value === '' || name.value === '' || displayName.value === '') {
15 | if (email.value === '') {
16 | email.classList.add('border-red-500', 'border-2');
17 | }
18 | if (name.value === '') {
19 | name.classList.add('border-red-500', 'border-2');
20 | }
21 | if (displayName.value === '') {
22 | displayName.classList.add('border-red-500', 'border-2');
23 | }
24 | } else {
25 | stepOne.classList.add('hidden');
26 | stepTwo.classList.remove('hidden');
27 | currentStep.textContent = 2;
28 | progressBar.classList.remove('w-1/3');
29 | progressBar.classList.add('w-2/3');
30 | }
31 | });
32 |
33 | email.addEventListener('blur', function () {
34 | email.classList.remove('border-red-500', 'border-2');
35 | });
36 | name.addEventListener('blur', function () {
37 | name.classList.remove('border-red-500', 'border-2');
38 | });
39 | displayName.addEventListener('blur', function () {
40 | displayName.classList.remove('border-red-500', 'border-2');
41 | });
42 |
43 | const checkEmail = () => {
44 | if (email.value.includes('@') && email.value.includes('.')) {
45 | email.classList.remove('border-red-500', 'border-2');
46 | email.classList.add('border-green-500', 'border-2');
47 | } else {
48 | email.classList.remove('border-green-500', 'border-2');
49 | email.classList.add('border-red-500', 'border-2');
50 | }
51 | }
52 | email.addEventListener('click', checkEmail);
53 | email.addEventListener('input', checkEmail);
54 | email.addEventListener('blur', checkEmail);
55 |
56 | let goBackBtnStepTwo = document.getElementById('backBtnStepTwo');
57 | let stepTwoBtn = document.getElementById('nextBtnStepTwo');
58 | let stepTwo = document.getElementById('stepTwo');
59 |
60 | let password = document.getElementById('password');
61 | let repeatPassword = document.getElementById('repeatPassword');
62 |
63 | goBackBtnStepTwo.addEventListener('click', function () {
64 | stepTwo.classList.add('hidden');
65 | stepOne.classList.remove('hidden');
66 | currentStep.textContent = 1;
67 | progressBar.classList.remove('w-2/3');
68 | progressBar.classList.add('w-1/3');
69 | });
70 |
71 | stepTwoBtn.addEventListener('click', function () {
72 | if (password.value === '' || repeatPassword.value === '') {
73 | if (password.value === '') {
74 | password.classList.add('border-red-500', 'border-2');
75 | }
76 | if (repeatPassword.value === '') {
77 | repeatPassword.classList.add('border-red-500', 'border-2');
78 | }
79 | } else {
80 | if (password.value === repeatPassword.value) {
81 | stepTwo.classList.add('hidden');
82 | stepThree.classList.remove('hidden');
83 | currentStep.textContent = 3;
84 | progressBar.classList.remove('w-2/3');
85 | progressBar.classList.add('w-full');
86 | } else {
87 | password.classList.add('border-red-500', 'border-2');
88 | repeatPassword.classList.add('border-red-500', 'border-2');
89 | window.alert('Your passwords do not match!');
90 | }
91 | }
92 | });
93 |
94 | password.addEventListener('blur', function () {
95 | password.classList.remove('border-red-500', 'border-2');
96 | });
97 |
98 | repeatPassword.addEventListener('blur', function () {
99 | repeatPassword.classList.remove('border-red-500', 'border-2');
100 | });
101 |
102 | let goBackBtnStepThree = document.getElementById('backBtnStepThree');
103 | let stepThree = document.getElementById('stepThree');
104 |
105 | goBackBtnStepThree.addEventListener('click', function () {
106 | stepThree.classList.add('hidden');
107 | stepTwo.classList.remove('hidden');
108 | currentStep.textContent = 2;
109 | progressBar.classList.remove('w-full');
110 | progressBar.classList.add('w-2/3');
111 | });
112 |
113 | let submitFormBtn = document.getElementById('submitFormBtn');
114 | let aboutMe = document.getElementById('aboutMe');
115 | let timezone = document.getElementById('timezone');
116 | let checkbox = document.getElementById('terms');
117 |
118 | const checkButton = () => {
119 | submitFormBtn.disabled = aboutMe.value === '' || checkbox.checked === false;
120 | }
121 |
122 | document.addEventListener('DOMContentLoaded', checkButton);
123 | aboutMe.addEventListener('input', checkButton);
124 | checkbox.addEventListener('click', checkButton);
125 |
126 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement.
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and an explanation of why the
80 | behavior was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series
85 | of actions.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the people involved, including unsolicited interaction with
89 | those enforcing the Code of Conduct, for a specified period of time. This
90 | includes avoiding interactions in community spaces as well as external channels
91 | like social media. Violating these terms may lead to a temporary or
92 | permanent ban.
93 |
94 | ### 3. Temporary Ban
95 |
96 | **Community Impact**: A serious violation of community standards, including
97 | sustained inappropriate behavior.
98 |
99 | **Consequence**: A temporary ban from any sort of interaction or public
100 | communication with the community for a specified period of time. No public or
101 | private interaction with the people involved, including unsolicited interaction
102 | with those enforcing the Code of Conduct, is allowed during this period.
103 | Violating these terms may lead to a permanent ban.
104 |
105 | ### 4. Permanent Ban
106 |
107 | **Community Impact**: Demonstrating a pattern of violation of community
108 | standards, including sustained inappropriate behavior, harassment of an
109 | individual, or aggression toward or disparagement of classes of individuals.
110 |
111 | **Consequence**: A permanent ban from any sort of public interaction within
112 | the community.
113 |
114 | ## Attribution
115 |
116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
117 | version 2.0, available at
118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
119 |
120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
121 | enforcement ladder](https://github.com/mozilla/diversity).
122 |
123 | [homepage]: https://www.contributor-covenant.org
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | https://www.contributor-covenant.org/faq. Translations are available at
127 | https://www.contributor-covenant.org/translations.
128 |
--------------------------------------------------------------------------------
/gmt/templates/articles/article.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}{{ article.title }}{% endblock %}
3 | {% block head %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {% endblock %}
16 | {% block body %}
17 |
18 |
19 | {{ article.title }}
20 |
21 | Published on: {{ article.date.strftime("%B %d, %Y") }}
22 |
23 |
24 | Written by:
25 | {{ article.author.name }}
27 |
28 |
29 |
31 |
32 | {{ content|safe }}
33 |
34 | {% if current_user.is_authenticated and current_user.writer.email == article.author.email %}
35 |
36 | Edit this article
38 |
39 |
53 |
58 | {% endif %}
59 |
60 |
61 | {% endblock %}
62 |
--------------------------------------------------------------------------------
/gmt/views/admin.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import pymongo
4 | import pytz
5 | from bson.objectid import ObjectId
6 |
7 | from flask import Flask, Blueprint, current_app
8 | from flask_admin import BaseView, expose
9 | from flask_login import current_user
10 |
11 | from .. import admin
12 | from .. import mongo
13 |
14 | from wtforms import form, fields
15 |
16 | from flask_admin.form import Select2Widget
17 | from flask_admin.contrib.pymongo import ModelView, filters
18 | from flask_admin.model.fields import InlineFormField, InlineFieldList
19 |
20 | # Create application
21 | bp = Blueprint("admins", __name__, url_prefix="/admin")
22 |
23 |
24 | class SecureModelView(ModelView):
25 | def is_accessible(self):
26 | if current_user.is_authenticated:
27 | current_user.writer = mongo.db.writers.find_one(
28 | {"_id": ObjectId(current_user.id)}
29 | )
30 | else:
31 | return False
32 |
33 | return current_user.writer["email"] in current_app.config["ADMIN_USER_EMAILS"]
34 |
35 |
36 | class UserForm(form.Form):
37 | confirmed = fields.BooleanField("confirmed")
38 | email = fields.StringField("email")
39 | time = fields.SelectField("time", choices=[i for i in range(24)])
40 | extras = fields.SelectMultipleField(
41 | "extras", choices=["codingchallenge", "repositories"]
42 | )
43 | password = fields.StringField("password")
44 | frequency = fields.SelectField(
45 | "frequency",
46 | choices=[
47 | ([1, 2, 3, 4, 5, 6, 7], "everyday"),
48 | ([1, 2, 3, 4, 5], "weekdays"),
49 | ([6, 7], "weekends"),
50 | ],
51 | )
52 | theme = fields.SelectField("theme", choices=[("light", "light"), ("dark", "dark")])
53 | timezone = fields.SelectField("timezone", choices=pytz.all_timezones)
54 |
55 |
56 | class UserView(SecureModelView):
57 | column_list = (
58 | "email",
59 | "confirmed",
60 | "time",
61 | "extras",
62 | "frequency",
63 | "theme",
64 | "timezone",
65 | )
66 |
67 | form = UserForm
68 |
69 |
70 | class ArticleForm(form.Form):
71 | title = fields.StringField("title")
72 | description = fields.StringField("description")
73 | content = fields.TextAreaField("content")
74 | thumbnail = fields.StringField("thumbnail")
75 | categories = fields.SelectMultipleField(
76 | "categories",
77 | choices=[
78 | ("ai-news", "AI"),
79 | ("corporation-news", "Corporation"),
80 | ("crypto-news", "Crypto"),
81 | ("gadget-news", "Gadget"),
82 | ("gaming-news", "Gaming"),
83 | ("robotics-news", "Robotics"),
84 | ("science-news", "Science"),
85 | ("space-news", "Space"),
86 | ("other-news", "Other"),
87 | ],
88 | )
89 | source = fields.SelectField(
90 | "source",
91 | choices=[
92 | ("gmt", "GMT"),
93 | ("techcrunch", "TechCrunch"),
94 | ("verge", "TheVerge"),
95 | ("bbc", "BBC"),
96 | ("cnn", "CNN"),
97 | ("guardian", "Guardian"),
98 | ],
99 | )
100 | formatted_source = fields.SelectField(
101 | "source",
102 | choices=[
103 | ("GMT", "GMT"),
104 | ("TechCrunch", "TechCrunch"),
105 | ("TheVerge", "TheVerge"),
106 | ("BBC", "BBC"),
107 | ("CNN", "CNN"),
108 | ("Guardian", "Guardian"),
109 | ],
110 | )
111 | author = fields.StringField("author")
112 | url = fields.StringField("url")
113 | views = fields.IntegerField("views", default=0)
114 | date = fields.DateTimeField("date", default=datetime.datetime.utcnow())
115 |
116 |
117 | class ArticleView(SecureModelView):
118 | column_list = (
119 | "title",
120 | "description",
121 | "content",
122 | "thumbnail",
123 | "categories",
124 | "source",
125 | "formatted_source",
126 | "author",
127 | "url",
128 | "views",
129 | "date",
130 | )
131 |
132 | form = ArticleForm
133 |
134 |
135 | class WriterForm(form.Form):
136 | about = fields.TextAreaField("about")
137 | accepted = fields.BooleanField("accepted")
138 | badges = fields.SelectMultipleField(
139 | "badges", choices=[("dev", "Dev"), ("writer", "Writer"), ("tester", "Tester")]
140 | )
141 | confirmed = fields.BooleanField("confirmed")
142 | created_at = fields.DateTimeField("created_at")
143 | email = fields.StringField("email")
144 | github = fields.StringField("github")
145 | name = fields.StringField("name")
146 | password = fields.PasswordField("password")
147 | patreon = fields.StringField("patreon")
148 | paypal = fields.StringField("paypal")
149 | public_email = fields.StringField("public_email")
150 | reasoning = fields.TextAreaField("reasoning")
151 | timezone = fields.SelectField("timezone", choices=pytz.all_timezones)
152 | twitter = fields.StringField("twitter")
153 | user_name = fields.StringField("user_name")
154 | views = fields.IntegerField("views")
155 | website = fields.StringField("website")
156 |
157 |
158 | class WriterView(SecureModelView):
159 | column_list = (
160 | "about",
161 | "accepted",
162 | "badges",
163 | "confirmed",
164 | "created_at",
165 | "email",
166 | "github",
167 | "name",
168 | "password",
169 | "patreon",
170 | "paypal",
171 | "public_email",
172 | "reasoning",
173 | "timezone",
174 | "twitter",
175 | "user_name",
176 | "views",
177 | "website",
178 | )
179 |
180 | form = WriterForm
181 |
182 |
183 | admin.add_view(UserView(mongo.db.users, "Users"))
184 | admin.add_view(ArticleView(mongo.db.articles, "Articles"))
185 | admin.add_view(WriterView(mongo.db.writers, "Writers"))
186 |
--------------------------------------------------------------------------------
/gmt/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import Flask, render_template
4 | from flask_mail import Mail
5 | from flask_pymongo import PyMongo
6 | from flask_turnstile import Turnstile
7 | from pymongo import MongoClient
8 | from flask_wtf.csrf import CSRFProtect
9 | from flask_session import Session
10 | from flask_mde import Mde
11 | from flask_login import LoginManager, UserMixin
12 | from flask_admin import Admin
13 |
14 | try:
15 | from flask_crontab import Crontab
16 | except ImportError:
17 | # @crontab.job(minute="*/30") create dummy cron job, to be crontab, FOR LOCAL DEVELOPMENT
18 | print("Crontab not installed, using dummy crontab")
19 |
20 | class Crontab:
21 | def job(self, **kwargs):
22 | def decorator(func):
23 | return func
24 |
25 | return decorator
26 |
27 | def init_app(self, app):
28 | pass
29 |
30 |
31 | crontab = Crontab()
32 | mail = Mail()
33 | mongo = PyMongo()
34 | csrf = CSRFProtect()
35 | sess = Session()
36 | mde = Mde()
37 | login_manager = LoginManager()
38 | turnstile = Turnstile()
39 | admin = Admin(name="Admin Page", template_mode="bootstrap4")
40 |
41 |
42 | class User(UserMixin):
43 | pass
44 |
45 |
46 | def create_app() -> Flask:
47 | """Create the Flask app.
48 |
49 | This function is responsible for creating the main Flask app, and it's the entry point for the factory pattern.
50 | """
51 | app = Flask(__name__, instance_relative_config=True)
52 |
53 | load_configuration(app)
54 | init_extensions(app)
55 | register_blueprints(app)
56 |
57 | return app
58 |
59 |
60 | def load_configuration(app: Flask) -> None:
61 | """Load the configuration.
62 |
63 | The configuration will be loaded either from a configuration file or from environment variables.
64 |
65 | The following variables can be configured:
66 | - SECRET_KEY: A secret key used for any security related needs. It should be a long random string.
67 | - SERVER_NAME: Inform the application what host and port it is bound to.
68 | - MONGO_URI: The connection URI to the MongoDB database. It should specify the database name as well.
69 | - MAIL_SERVER: The SMTP server to connect to.
70 | - MAIL_PORT: The port of the SMTP server.
71 | - MAIL_USE_TLS: True if TLS is to be used.
72 | - MAIL_USE_SSL: True if SSL is to be used.
73 | - MAIL_USERNAME: The email address to send the mail from.
74 | - MAIL_PASSWORD: The password of the email address.
75 | - WRITER_WEBHOOK: The URL of the Discord webhook to send writer apply requests.
76 | - FORM_WEBHOOK: The URL of the Discord webhook to send form requests.
77 | """
78 | app.config["FLASK_ADMIN_SWATCH"] = "lux"
79 | app.config["SESSION_TYPE"] = "mongodb"
80 | app.config["SESSION_MONGODB_DB"] = "goodmorningtech"
81 | app.config["SESSION_MONGODB_COLLECT"] = "sessions"
82 | try:
83 | app.config.from_pyfile("config.py")
84 | app.config["SESSION_MONGODB"] = MongoClient(app.config["MONGO_URI"])
85 | except OSError:
86 | app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
87 | app.config["DOMAIN_NAME"] = os.environ.get("DOMAIN_NAME")
88 | app.config["MONGO_URI"] = os.environ.get("MONGO_URI")
89 | app.config["MAIL_SERVER"] = os.environ.get("MAIL_SERVER")
90 | app.config["MAIL_PORT"] = os.environ.get("MAIL_PORT")
91 | app.config["MAIL_USE_TLS"] = os.environ.get("MAIL_USE_TLS")
92 | app.config["MAIL_USE_SSL"] = os.environ.get("MAIL_USE_SSL")
93 | app.config["MAIL_USERNAME"] = os.environ.get("MAIL_USERNAME")
94 | app.config["MAIL_PASSWORD"] = os.environ.get("MAIL_PASSWORD")
95 | app.config["WRITER_WEBHOOK"] = os.environ.get("WRITER_WEBHOOK")
96 | app.config["FORM_WEBHOOK"] = os.environ.get("FORM_WEBHOOK")
97 | app.config["CRON_JOB_WEBHOOK"] = os.environ.get("CRON_JOB_WEBHOOK")
98 | app.config["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY")
99 | app.config["FTP_USER"] = os.environ.get("FTP_USER")
100 | app.config["FTP_PASSWORD"] = os.environ.get("FTP_PASSWORD")
101 | app.config["FTP_HOST"] = os.environ.get("FTP_HOST")
102 | app.config["API_NINJA_KEY"] = os.environ.get("API_NINJA_KEY")
103 | app.config["MISTRAL_API_KEY"] = os.environ.get("MISTRAL_API_KEY")
104 | app.config["ADMIN_USER_EMAILS"] = (
105 | os.environ.get("ADMIN_USER_EMAILS").split(",")
106 | if os.environ.get("ADMIN_USER_EMAILS")
107 | else []
108 | )
109 | app.config["SESSION_MONGODB"] = MongoClient(app.config["MONGO_URI"])
110 |
111 | app.config["TURNSTILE_SITE_KEY"] = os.environ.get("TURNSTILE_SITE_KEY")
112 | app.config["TURNSTILE_SECRET_KEY"] = os.environ.get("TURNSTILE_SECRET_KEY")
113 |
114 | if app.config["MAIL_PORT"]:
115 | app.config["MAIL_PORT"] = int(app.config["MAIL_PORT"])
116 | if app.config["MAIL_USE_TLS"]:
117 | app.config["MAIL_USE_TLS"] = app.config["MAIL_USE_TLS"].casefold() == "true"
118 | if app.config["MAIL_USE_SSL"]:
119 | app.config["MAIL_USE_SSL"] = app.config["MAIL_USE_SSL"].casefold() == "true"
120 |
121 |
122 | def init_extensions(app: Flask) -> None:
123 | """Initialize Flask extensions."""
124 | csrf.init_app(app)
125 | mail.init_app(app)
126 | mongo.init_app(app)
127 | sess.init_app(app)
128 | mde.init_app(app)
129 | login_manager.init_app(app)
130 | admin.init_app(app)
131 | crontab.init_app(app)
132 | turnstile.init_app(app)
133 |
134 |
135 | def register_blueprints(app: Flask) -> None:
136 | """Register Flask blueprints."""
137 | from .views import articles, auth, commands, general, writers, admin, api
138 |
139 | @app.errorhandler(404)
140 | def page_not_found(_):
141 | return render_template("404.html")
142 |
143 | app.register_blueprint(articles.bp)
144 | app.register_blueprint(auth.bp)
145 | app.register_blueprint(commands.bp)
146 | app.register_blueprint(general.bp)
147 | app.register_blueprint(writers.bp)
148 | app.register_blueprint(admin.bp)
149 | app.register_blueprint(api.bp)
150 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Good Morning Tech
2 |
3 |
4 |
5 | We are an open-source tech newsletter, sign up and stay updated with the latest news in tech at your convenience! Oh did I mention, we are 100% free?
6 | Checkout our website • Get in touch with us • Report a bug
7 |
8 |
9 |
10 |
11 |
12 | 
13 | 
14 | 
15 | 
16 |
17 |
18 | 
19 | 
20 | 
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Table of Content:
31 |
32 | -
33 | Learn more about this project
34 |
35 | - Built With
36 | - Features
37 |
38 |
39 | -
40 | Get started
41 |
42 | - Contribute
43 | - Setting up on your local machine
44 |
45 |
46 | - What's planned ahead
47 | - Frequently Asked Question's (FAQs)
48 | - License
49 | - Contact Us
50 | - Our team
51 |
52 |
53 |
54 |
55 | ## Learn more about this project
56 |
57 |
58 |
59 |
60 | ### Built With
61 | 
62 | 
63 | 
64 |
65 | 
66 | 
67 | 
68 | 
69 |
70 | 
71 | 
72 | 
73 |
74 | 
75 | 
76 | 
77 |
78 |
79 |
80 | ### Features
81 |
82 | - Timezone Selection
83 | - Day and time Selection
84 | - Article Count Selection
85 |
86 | ## Get started
87 | ### Contribute
88 | Contributing to this project is quite simple & straight forward. We'd request you to view our [contributing](https://github.com/GoodMorninTech/GoodMorningTech/blob/master/CONTRIBUTING.md) file before getting started and follow our [code of conduct](https://github.com/GoodMorninTech/GoodMorningTech/blob/master/CODE_OF_CONDUCT.md).
89 |
90 | ### Setting up on your local machine
91 | [Check out this guide](https://github.com/GoodMorninTech/GoodMorningTech/blob/master/CONTRIBUTING.md#getting-started)
92 |
93 | ## What's Planned Ahead:
94 | - [x] Time Selection
95 | - [x] Timezone Selection
96 | - [x] Addition of more news sources
97 | - [x] Blogging System
98 | - [ ] Changelog System
99 | - [ ] Support for Other Languages
100 | - [ ] French
101 | - [ ] German
102 | - [ ] Spanish
103 | - [ ] Mobile App
104 |
105 | ## Frequently Asked Question's (FAQs):
106 |
107 | #### 1. How does this work?
108 |
109 | It gets the important posts from BBC, The Guardian, Verge & other credible sources and sends them to your email.
110 |
111 | #### 2. How do I subscribe?
112 |
113 | Subscribing is as easy as heading to our [sign up page](https://goodmorningtech.news/subscribe) and giving us your email & filling a small form (we promise we won't flood your inbox).
114 |
115 | #### 3. How do I unsubscribe?
116 |
117 | We hate to see you leave, you can head to [this page](https://goodmorningtech.news/unsubscribe) and enter your email ID, we'll then send you a link to verify your exit. Alternatively, each newsletter we send you has a footer with an unsubscribe link.
118 |
119 | #### 4. How do you guys fund your project if it's completely free?
120 | We rely on donations/sponsors!
121 |
122 | ## License
123 |
124 | [MIT](https://choosealicense.com/licenses/mit/)
125 |
126 |
127 | ## Contact Us
128 | 
129 | 
130 | 
131 |
132 |
133 | ## Our team
134 | - [OpenSourceSimon](https://github.com/OpenSourceSimon) - Backend
135 | - [Kappq](https://github.com/kappq) - Backend
136 | - [ImmaHarry](https://github.com/immaharry) - Site Designer & Frontend
137 | - [LevaniVashadze](https://github.com/LevaniVashadze) - Backend & Frontend
138 |
--------------------------------------------------------------------------------
/gmt/templates/writers/portal.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Portal{% endblock %}
3 | {% block body %}
4 |
5 |
6 |
7 | Writer Portal
8 | Here you can write your own articles.
9 |
10 | {% if profile_picture %}
11 |
14 | {% else %}
15 |
22 | {% endif %}
23 |
24 |
25 |
41 |
44 |
54 | Settings
55 |
56 |
59 |
69 | Delete Profile
70 |
71 |
72 |
73 |
74 | Articles
75 |
76 | Click here to write a new article.
77 |
78 |
79 | {% for article in articles %}
80 | -
81 | - {{ article.title }}
83 |
84 | {% endfor %}
85 |
86 |
87 |
88 |
89 | {% endblock %}
90 |
--------------------------------------------------------------------------------
/gmt/templates/general/morning.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Morning{% endblock %}
3 | {% block head %}
4 |
6 |
8 |
9 |
10 |
11 |
13 |
14 |
16 |
17 | {% endblock %}
18 | {% block body %}
19 |
35 |
36 |
38 |
39 |
40 | Get tech news to your
41 | Discord Community
42 |
43 |
44 | Get the latest tech news straight to your Discord
45 | community on a channel of your choice! Get all of the customizing options offered on the email
46 | newsletter using our open-source Discord Bot,
47 | Morning!
48 |
49 |
50 |
52 | Add to Discord
53 |
54 |
56 | How to setup
57 |
58 |
59 |
60 |
65 |
70 |
71 |
73 |
74 |
75 | How to set it up:
76 |
77 |
78 |
79 |
80 |
81 |
83 |
84 |
85 |
86 | Inviting Morning
87 |
88 |
89 | Invite Morning, our official Discord bot to your Discord server by clicking the button below, this should take you 5 seconds!
90 |
91 |
93 | Invite Morning
94 |
95 |
96 |
97 |
98 |
100 |
101 |
102 |
103 | Setting up Morning
104 |
105 |
106 | Run the
107 | /setup create command to setup the bot on your server, select everything you wish the bot should deliver!
108 |
109 |
110 |
111 |
112 |
113 | {% endblock %}
114 |
--------------------------------------------------------------------------------
/gmt/templates/general/contact.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Contact Us{% endblock %}
3 | {% block head %}
4 |
6 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
17 |
18 |
19 | {% endblock %}
20 | {% block body %}
21 | {# #}
29 |
30 |
31 |
33 | Get in Touch!
34 |
35 |
36 | Have any queries? Fill out this small form & we’ll get back to you as soon as we can!
37 |
38 |
39 |
40 | support@goodmorningtech.news
41 |
42 |
53 |
54 |
55 |
56 |
123 |
126 |
127 |
128 | {% endblock %}
129 |
--------------------------------------------------------------------------------
/gmt/views/general.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import random
3 | import re
4 |
5 | from bson import ObjectId
6 | from email_validator import validate_email, EmailNotValidError
7 | from flask import Blueprint, render_template, redirect, request, url_for, current_app
8 | from flask_mail import Message
9 | from werkzeug import Response
10 | from markdown import markdown
11 | from flask_login import login_required, current_user
12 |
13 | from ..news import get_news
14 | from .. import mongo, login_manager, User, mail
15 | from ..utils import random_language_greeting
16 | from ..extras import get_daily_coding_challenge, get_trending_repos, get_surprise
17 |
18 | bp = Blueprint("general", __name__)
19 |
20 |
21 | @bp.route("/", methods=["GET", "POST"])
22 | def index():
23 | """Render the home page."""
24 | if request.method == "POST":
25 | email = request.form.get("email")
26 | if email:
27 | return redirect(url_for("auth.subscribe", email=email))
28 |
29 | if current_user.is_authenticated:
30 | current_user.writer = mongo.db.writers.find_one(
31 | {"_id": ObjectId(current_user.id)}
32 | )
33 |
34 | posts = mongo.db.articles.find(
35 | {"date": {"$gte": datetime.datetime.utcnow() - datetime.timedelta(hours=25)}}
36 | )
37 |
38 | # Mix the posts
39 | posts = list(posts)
40 | if len(posts) > 2:
41 | # Gets a random post and removes it from the list
42 | post1 = random.choice(posts)
43 | posts.remove(post1)
44 | # Gets a second random post
45 | post2 = random.choice(posts)
46 |
47 | # set limits for the description to 360 characters and add more length for each [link] tag
48 | limit1 = 360 + post1["description"][:360].count("[link]") * 30
49 | limit2 = 360 + post2["description"][:360].count("[link]") * 30
50 |
51 | # slice the description to the limit
52 | post1["description"] = post1["description"][:limit1]
53 | post2["description"] = post2["description"][:limit2]
54 |
55 | # remove unterminated [link] tags
56 | if re.search("\[link\]\([^\)]*[^\)]$", post1["description"]):
57 | post1["description"] = re.sub(
58 | "\[link\]\(.*[^\)]$", "", post1["description"]
59 | )
60 | if re.search("\[link\]\([^\)]*[^\)]$", post2["description"]):
61 | post2["description"] = re.sub(
62 | "\[link\]\([^\)]*[^\)]$", "", post2["description"]
63 | )
64 |
65 | # add ellipses and markdown it
66 | post1["description"] = markdown(post1["description"] + "...")
67 | post2["description"] = markdown(post2["description"] + "...")
68 | posts = [post1, post2]
69 | else:
70 | posts = []
71 |
72 | return render_template("general/index.html", news=posts)
73 |
74 |
75 | @bp.route("/news")
76 | def news():
77 | """Render the newspaper."""
78 | posts = list(
79 | mongo.db.articles.find(
80 | {
81 | "date": {
82 | "$gte": datetime.datetime.utcnow() - datetime.timedelta(hours=25)
83 | }
84 | }
85 | )
86 | )
87 |
88 | if not posts:
89 | posts = get_news(choice="BBC")
90 |
91 | random.shuffle(posts)
92 |
93 | return render_template(
94 | "general/news.html",
95 | posts=posts[:12],
96 | theme="light",
97 | markdown=markdown,
98 | domain_name=current_app.config["DOMAIN_NAME"],
99 | repos=get_trending_repos(),
100 | coding_challenge=get_daily_coding_challenge(),
101 | surprise=get_surprise(),
102 | random_language_greeting=random_language_greeting(),
103 | )
104 |
105 |
106 | @bp.route("/about")
107 | def about():
108 | if current_user.is_authenticated:
109 | current_user.writer = mongo.db.writers.find_one(
110 | {"_id": ObjectId(current_user.id)}
111 | )
112 | return render_template("general/about.html", no_meta=True)
113 |
114 |
115 | @bp.route("/contact", methods=["GET", "POST"])
116 | def contact():
117 | if current_user.is_authenticated:
118 | current_user.writer = mongo.db.writers.find_one(
119 | {"_id": ObjectId(current_user.id)}
120 | )
121 | if request.method == "POST":
122 | fake_email = request.form.get("email")
123 | if fake_email:
124 | return render_template(
125 | "general/contact.html",
126 | error="Invalid email address",
127 | success=False,
128 | no_meta=True,
129 | )
130 | email = request.form.get("real_email")
131 | name = request.form.get("name")
132 | subject = request.form.get("subject")
133 | message = request.form.get("message")
134 | try:
135 | validate_email(email)
136 | except EmailNotValidError as e:
137 | return render_template(
138 | "general/contact.html",
139 | error="Invalid email address",
140 | success=False,
141 | no_meta=True,
142 | )
143 | else:
144 | msg = Message(
145 | subject=f"Contact Form Submission from {name} - {subject}",
146 | sender=("Good Morning Tech", current_app.config["MAIL_USERNAME"]),
147 | recipients=["support@goodmorningtech.news"],
148 | body=f"From: {name} <{email}>,\n{message}",
149 | )
150 | mail.send(msg)
151 | return render_template(
152 | "general/contact.html", success=True, error=None, no_meta=True
153 | )
154 | if current_user.is_authenticated:
155 | current_user.writer = mongo.db.writers.find_one(
156 | {"_id": ObjectId(current_user.id)}
157 | )
158 | return render_template(
159 | "general/contact.html", success=False, error=None, no_meta=True
160 | )
161 |
162 |
163 | @bp.route("/contribute")
164 | def contribute():
165 | if current_user.is_authenticated:
166 | current_user.writer = mongo.db.writers.find_one(
167 | {"_id": ObjectId(current_user.id)}
168 | )
169 | return render_template("general/contribute.html")
170 |
171 |
172 | @bp.route("/morning")
173 | def morning():
174 | if current_user.is_authenticated:
175 | current_user.writer = mongo.db.writers.find_one(
176 | {"_id": ObjectId(current_user.id)}
177 | )
178 | return render_template("general/morning.html", no_meta=True)
179 |
180 |
181 | @bp.route("/privacy")
182 | def privacy():
183 | if current_user.is_authenticated:
184 | current_user.writer = mongo.db.writers.find_one(
185 | {"_id": ObjectId(current_user.id)}
186 | )
187 | return render_template("general/privacy_policy.html")
188 |
189 |
190 | @bp.route("/tos")
191 | def terms():
192 | if current_user.is_authenticated:
193 | current_user.writer = mongo.db.writers.find_one(
194 | {"_id": ObjectId(current_user.id)}
195 | )
196 | return render_template("general/tos.html")
197 |
198 |
199 | @bp.route("/credits")
200 | def credits():
201 | if current_user.is_authenticated:
202 | current_user.writer = mongo.db.writers.find_one(
203 | {"_id": ObjectId(current_user.id)}
204 | )
205 | return render_template("general/credits.html")
206 |
207 |
208 | @bp.route("/sitemap.xml")
209 | def sitemap():
210 | """Render the sitemap.xml."""
211 |
212 | sitemap_xml = render_template("general/sitemap.xml")
213 | response = Response(sitemap_xml, mimetype="text/xml")
214 | response.headers["Content-Type"] = "application/xml"
215 |
216 | return response
217 |
218 |
219 | @bp.route("/robots.txt")
220 | def robots():
221 | """Render the robots.txt."""
222 | robots_txt = render_template("general/robots.txt")
223 | response = Response(robots_txt, mimetype="text/plain")
224 | response.headers["Content-Type"] = "text/plain"
225 |
226 | return response
227 |
228 |
229 | @login_manager.user_loader
230 | def load_user(user_id):
231 | user_doc = mongo.db.writers.find_one({"_id": ObjectId(user_id)})
232 | if user_doc:
233 | user = User()
234 | user.id = str(user_doc["_id"])
235 | return user
236 | else:
237 | return None
238 |
239 |
240 | @login_manager.unauthorized_handler
241 | def unauthorized_callback():
242 | return redirect(url_for("writers.login"))
243 |
--------------------------------------------------------------------------------
/gmt/extras.py:
--------------------------------------------------------------------------------
1 | import random
2 | import bs4
3 | import requests
4 | from flask import current_app
5 |
6 | from .utils import format_html
7 |
8 |
9 | def filter_articles(raw_html: str) -> str:
10 | """Filters HTML out, which is not enclosed by article-tags.
11 | Beautifulsoup is inaccurate and slow when applied on a larger
12 | HTML string, this filtration fixes this.
13 | """
14 | raw_html_lst = raw_html.split("\n")
15 |
16 | # count number of article tags within the document (varies from 0 to 50):
17 | article_tags_count = 0
18 | tag = "article"
19 | for line in raw_html_lst:
20 | if tag in line:
21 | article_tags_count += 1
22 |
23 | # copy HTML enclosed by first and last article-tag:
24 | articles_arrays, is_article = [], False
25 | for line in raw_html_lst:
26 | if tag in line:
27 | article_tags_count -= 1
28 | is_article = True
29 | if is_article:
30 | articles_arrays.append(line)
31 | if not article_tags_count:
32 | is_article = False
33 | return "".join(articles_arrays)
34 |
35 |
36 | def make_soup(articles_html: str) -> bs4.element.ResultSet:
37 | """HTML enclosed by article-tags is converted into a
38 | soup for further data extraction.
39 | """
40 | soup = bs4.BeautifulSoup(articles_html, "lxml")
41 | return soup.find_all("article", class_="Box-row")
42 |
43 |
44 | def scraping_repositories(
45 | matches: bs4.element.ResultSet,
46 | since: str,
47 | ):
48 | """Data about all trending repositories are extracted."""
49 | trending_repositories = []
50 | for rank, match in enumerate(matches):
51 | # description
52 | if match.p:
53 | description = match.p.get_text(strip=True)
54 | else:
55 | description = None
56 |
57 | # relative url:
58 | rel_url = match.select_one("h2 > a")["href"]
59 |
60 | # absolute url:
61 | repo_url = "https://github.com" + rel_url
62 |
63 | # name of repo
64 | repository_name = rel_url.split("/")[-1]
65 |
66 | # author (username):
67 | username = rel_url.split("/")[-2]
68 |
69 | # language and color
70 | progr_language = match.find("span", itemprop="programmingLanguage")
71 | if progr_language:
72 | language = progr_language.get_text(strip=True)
73 | lang_color_tag = match.find("span", class_="repo-language-color")
74 | lang_color = lang_color_tag["style"].split()[-1]
75 | else:
76 | lang_color, language = None, None
77 |
78 | stars_built_section = match.div.findNextSibling("div")
79 |
80 | # total stars:
81 | if stars_built_section.a:
82 | raw_total_stars = stars_built_section.a.get_text(strip=True)
83 | if "," in raw_total_stars:
84 | raw_total_stars = raw_total_stars.replace(",", "")
85 | if raw_total_stars:
86 | total_stars: int
87 | try:
88 | total_stars = int(raw_total_stars)
89 | except ValueError as missing_number:
90 | print(missing_number)
91 | else:
92 | total_stars = None
93 |
94 | # forks
95 | if stars_built_section.a.findNextSibling("a"):
96 | raw_forks = stars_built_section.a.findNextSibling(
97 | "a",
98 | ).get_text(strip=True)
99 | if "," in raw_forks:
100 | raw_forks = raw_forks.replace(",", "")
101 | if raw_forks:
102 | forks: int
103 | try:
104 | forks = int(raw_forks)
105 | except ValueError as missing_number:
106 | print(missing_number)
107 | else:
108 | forks = None
109 |
110 | # stars in period
111 | if stars_built_section.find(
112 | "span",
113 | class_="d-inline-block float-sm-right",
114 | ):
115 | raw_stars_since = (
116 | stars_built_section.find(
117 | "span",
118 | class_="d-inline-block float-sm-right",
119 | )
120 | .get_text(strip=True)
121 | .split()[0]
122 | )
123 | if "," in raw_stars_since:
124 | raw_stars_since = raw_stars_since.replace(",", "")
125 | if raw_stars_since:
126 | stars_since: int
127 | try:
128 | stars_since = int(raw_stars_since)
129 | except ValueError as missing_number:
130 | print(missing_number)
131 | else:
132 | stars_since = None
133 |
134 | # builtby
135 | built_section = stars_built_section.find(
136 | "span",
137 | class_="d-inline-block mr-3",
138 | )
139 | if built_section:
140 | contributors = stars_built_section.find(
141 | "span",
142 | class_="d-inline-block mr-3",
143 | ).find_all("a")
144 | built_by = []
145 | for contributor in contributors:
146 | contr_data = {}
147 | contr_data["username"] = contributor["href"].strip("/")
148 | contr_data["url"] = "https://github.com" + contributor["href"]
149 | contr_data["avatar"] = contributor.img["src"]
150 | built_by.append(dict(contr_data))
151 | else:
152 | built_by = None
153 |
154 | repositories = {
155 | "rank": rank + 1,
156 | "username": username,
157 | "name": repository_name,
158 | "whole_name": username + "/" + repository_name,
159 | "url": repo_url,
160 | "description": description,
161 | "language": language,
162 | "language_color": lang_color,
163 | "total_stars": total_stars,
164 | "forks": forks,
165 | "stars_since": stars_since,
166 | "since": since,
167 | "built_by": built_by,
168 | }
169 | trending_repositories.append(repositories)
170 |
171 | return trending_repositories
172 |
173 |
174 | def get_trending_repos(since="daily"):
175 | payload = {"since": since} # "daily", "weekly", "monthly", "yearly"
176 |
177 | url = "https://github.com/trending"
178 | raw_html = requests.get(url, params=payload).text
179 |
180 | articles_html = filter_articles(raw_html)
181 | soup = make_soup(articles_html)
182 | trending_repos = scraping_repositories(soup, since=payload["since"])
183 |
184 | return trending_repos[:4]
185 |
186 |
187 | def get_daily_coding_challenge():
188 | headers = {
189 | "Content-Type": "application/json",
190 | }
191 |
192 | json_data = {
193 | "query": "query questionOfToday {\n\tactiveDailyCodingChallengeQuestion {\n\t\tdate\n\t\tuserStatus\n\t\tlink\n\t\tquestion {\n\t\t\tacRate\n\t\t\tdifficulty\n\t\t\tfreqBar\n\t\t\tfrontendQuestionId: questionFrontendId\n\t\t\tisFavor\n\t\t\tpaidOnly: isPaidOnly\n\t\t\tstatus\n\t\t\ttitle\n\t\t\ttitleSlug\n\t\t\thasVideoSolution\n\t\t\thasSolution\n\t\t\ttopicTags {\n\t\t\t\tname\n\t\t\t\tid\n\t\t\t\tslug\n\t\t\t}\n\t\t}\n\t}\n}\n",
194 | "operationName": "questionOfToday",
195 | }
196 |
197 | response = requests.post(
198 | "https://leetcode.com/graphql", headers=headers, json=json_data
199 | )
200 | json_response = response.json()
201 | title_slug = json_response["data"]["activeDailyCodingChallengeQuestion"][
202 | "question"
203 | ]["titleSlug"]
204 |
205 | json_data = {
206 | "query": "\n query questionContent($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n content\n mysqlSchemas\n }\n}\n ",
207 | "variables": {"titleSlug": title_slug},
208 | "operationName": "questionContent",
209 | }
210 |
211 | response = requests.post(
212 | "https://leetcode.com/graphql", headers=headers, json=json_data
213 | )
214 | json_response = response.json()
215 |
216 | title = " ".join(word.capitalize() for word in title_slug.split("-"))
217 | raw_content = json_response["data"]["question"]["content"]
218 | description = (
219 | format_html(raw_content).replace("", "").replace("
", "")
220 | )
221 | return {"title": title, "description": description}
222 |
223 |
224 | def get_surprise():
225 | randomizer = random.randint(0, 2)
226 | try:
227 | if randomizer == 0:
228 | joke = requests.get(
229 | "https://v2.jokeapi.dev/joke/Programming,Miscellaneous,Pun?blacklistFlags=nsfw,religious,racist,sexist,explicit"
230 | ).json()
231 | if joke["type"] == "single":
232 | return "Today's joke:\n" + joke["joke"]
233 | else:
234 | return "Today's joke:\n" + joke["setup"] + "\n" + joke["delivery"]
235 | elif randomizer == 1:
236 | quote = requests.get("https://api.quotable.io/quotes/random").json()[0]
237 | return "Today's quote:\n" + quote["content"] + "\n-" + quote["author"]
238 | else:
239 | api_url = "https://api.api-ninjas.com/v1/facts?limit=1"
240 | headers = {
241 | "X-Api-Key": current_app.config["API_NINJA_KEY"],
242 | "Accept": "application/json",
243 | }
244 | response = requests.get(api_url, headers=headers)
245 | fact = response.json()
246 | return "Today's Fact:\n" + fact[0]["fact"]
247 | except Exception as e:
248 | print(e)
249 | return "Sorry, I couldn't get a surprise for you today :( If this occurs again, please contact us."
250 |
--------------------------------------------------------------------------------
/gmt/static/loader.css:
--------------------------------------------------------------------------------
1 | /***************************************************
2 | * Generated by SVG Artista on 11/16/2022, 7:47:19 PM
3 | * MIT license (https://opensource.org/licenses/MIT)
4 | * W. https://svgartista.net
5 | **************************************************/
6 |
7 | @-webkit-keyframes animate-svg-stroke-1 {
8 | 0% {
9 | stroke-dashoffset: 396.869384765625px;
10 | stroke-dasharray: 396.869384765625px;
11 | }
12 |
13 | 100% {
14 | stroke-dashoffset: 0;
15 | stroke-dasharray: 396.869384765625px;
16 | }
17 | }
18 |
19 | @keyframes animate-svg-stroke-1 {
20 | 0% {
21 | stroke-dashoffset: 396.869384765625px;
22 | stroke-dasharray: 396.869384765625px;
23 | }
24 |
25 | 100% {
26 | stroke-dashoffset: 0;
27 | stroke-dasharray: 396.869384765625px;
28 | }
29 | }
30 |
31 | .gmt-main-icon-1 {
32 | -webkit-animation: animate-svg-stroke-1 1s ease-in 0s both,
33 | animate-svg-fill-1 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.8s both;
34 | animation: animate-svg-stroke-1 1s ease-in 0s both,
35 | animate-svg-fill-1 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.8s both;
36 | }
37 |
38 | @-webkit-keyframes animate-svg-stroke-2 {
39 | 0% {
40 | stroke-dashoffset: 334.7112731933594px;
41 | stroke-dasharray: 334.7112731933594px;
42 | }
43 |
44 | 100% {
45 | stroke-dashoffset: 0;
46 | stroke-dasharray: 334.7112731933594px;
47 | }
48 | }
49 |
50 | @keyframes animate-svg-stroke-2 {
51 | 0% {
52 | stroke-dashoffset: 334.7112731933594px;
53 | stroke-dasharray: 334.7112731933594px;
54 | }
55 |
56 | 100% {
57 | stroke-dashoffset: 0;
58 | stroke-dasharray: 334.7112731933594px;
59 | }
60 | }
61 |
62 | @-webkit-keyframes animate-svg-fill-2 {
63 | 0% {
64 | fill: transparent;
65 | }
66 |
67 | 100% {
68 | fill: rgb(207, 51, 51);
69 | }
70 | }
71 |
72 | @keyframes animate-svg-fill-2 {
73 | 0% {
74 | fill: transparent;
75 | }
76 |
77 | 100% {
78 | fill: rgb(207, 51, 51);
79 | }
80 | }
81 |
82 | .gmt-main-icon-2 {
83 | -webkit-animation: animate-svg-stroke-2 1s ease-in 0.12s both,
84 | animate-svg-fill-2 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.9s both;
85 | animation: animate-svg-stroke-2 1s ease-in 0.12s both,
86 | animate-svg-fill-2 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.9s both;
87 | }
88 |
89 | @-webkit-keyframes animate-svg-stroke-3 {
90 | 0% {
91 | stroke-dashoffset: 341.52288818359375px;
92 | stroke-dasharray: 341.52288818359375px;
93 | }
94 |
95 | 100% {
96 | stroke-dashoffset: 0;
97 | stroke-dasharray: 341.52288818359375px;
98 | }
99 | }
100 |
101 | @keyframes animate-svg-stroke-3 {
102 | 0% {
103 | stroke-dashoffset: 341.52288818359375px;
104 | stroke-dasharray: 341.52288818359375px;
105 | }
106 |
107 | 100% {
108 | stroke-dashoffset: 0;
109 | stroke-dasharray: 341.52288818359375px;
110 | }
111 | }
112 |
113 | .gmt-main-icon-3 {
114 | -webkit-animation: animate-svg-stroke-3 1s ease-in 0.24s both,
115 | animate-svg-fill-3 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1s both;
116 | animation: animate-svg-stroke-3 1s ease-in 0.24s both,
117 | animate-svg-fill-3 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1s both;
118 | }
119 |
120 | @-webkit-keyframes animate-svg-stroke-4 {
121 | 0% {
122 | stroke-dashoffset: 90.84814453125px;
123 | stroke-dasharray: 90.84814453125px;
124 | }
125 |
126 | 100% {
127 | stroke-dashoffset: 0;
128 | stroke-dasharray: 90.84814453125px;
129 | }
130 | }
131 |
132 | @keyframes animate-svg-stroke-4 {
133 | 0% {
134 | stroke-dashoffset: 90.84814453125px;
135 | stroke-dasharray: 90.84814453125px;
136 | }
137 |
138 | 100% {
139 | stroke-dashoffset: 0;
140 | stroke-dasharray: 90.84814453125px;
141 | }
142 | }
143 |
144 | .gmt-main-icon-4 {
145 | -webkit-animation: animate-svg-stroke-4 1s ease-in 0.36s both,
146 | animate-svg-fill-4 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.1s both;
147 | animation: animate-svg-stroke-4 1s ease-in 0.36s both,
148 | animate-svg-fill-4 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.1s both;
149 | }
150 |
151 | @-webkit-keyframes animate-svg-stroke-5 {
152 | 0% {
153 | stroke-dashoffset: 90.84815216064453px;
154 | stroke-dasharray: 90.84815216064453px;
155 | }
156 |
157 | 100% {
158 | stroke-dashoffset: 0;
159 | stroke-dasharray: 90.84815216064453px;
160 | }
161 | }
162 |
163 | @keyframes animate-svg-stroke-5 {
164 | 0% {
165 | stroke-dashoffset: 90.84815216064453px;
166 | stroke-dasharray: 90.84815216064453px;
167 | }
168 |
169 | 100% {
170 | stroke-dashoffset: 0;
171 | stroke-dasharray: 90.84815216064453px;
172 | }
173 | }
174 |
175 | .gmt-main-icon-5 {
176 | -webkit-animation: animate-svg-stroke-5 1s ease-in 0.48s both,
177 | animate-svg-fill-5 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.2000000000000002s both;
178 | animation: animate-svg-stroke-5 1s ease-in 0.48s both,
179 | animate-svg-fill-5 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.2000000000000002s both;
180 | }
181 |
182 | @-webkit-keyframes animate-svg-stroke-6 {
183 | 0% {
184 | stroke-dashoffset: 90.84815979003906px;
185 | stroke-dasharray: 90.84815979003906px;
186 | }
187 |
188 | 100% {
189 | stroke-dashoffset: 0;
190 | stroke-dasharray: 90.84815979003906px;
191 | }
192 | }
193 |
194 | @keyframes animate-svg-stroke-6 {
195 | 0% {
196 | stroke-dashoffset: 90.84815979003906px;
197 | stroke-dasharray: 90.84815979003906px;
198 | }
199 |
200 | 100% {
201 | stroke-dashoffset: 0;
202 | stroke-dasharray: 90.84815979003906px;
203 | }
204 | }
205 |
206 | .gmt-main-icon-6 {
207 | -webkit-animation: animate-svg-stroke-6 1s ease-in 0.6s both,
208 | animate-svg-fill-6 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.3s both;
209 | animation: animate-svg-stroke-6 1s ease-in 0.6s both,
210 | animate-svg-fill-6 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.3s both;
211 | }
212 |
213 | @-webkit-keyframes animate-svg-stroke-7 {
214 | 0% {
215 | stroke-dashoffset: 959.854736328125px;
216 | stroke-dasharray: 959.854736328125px;
217 | }
218 |
219 | 100% {
220 | stroke-dashoffset: 0;
221 | stroke-dasharray: 959.854736328125px;
222 | }
223 | }
224 |
225 | @keyframes animate-svg-stroke-7 {
226 | 0% {
227 | stroke-dashoffset: 959.854736328125px;
228 | stroke-dasharray: 959.854736328125px;
229 | }
230 |
231 | 100% {
232 | stroke-dashoffset: 0;
233 | stroke-dasharray: 959.854736328125px;
234 | }
235 | }
236 |
237 | @-webkit-keyframes animate-svg-fill-7 {
238 | 0% {
239 | fill: transparent;
240 | }
241 |
242 | 100% {
243 | fill: rgb(0, 0, 0);
244 | }
245 | }
246 |
247 | @keyframes animate-svg-fill-7 {
248 | 0% {
249 | fill: transparent;
250 | }
251 |
252 | 100% {
253 | fill: rgb(0, 0, 0);
254 | }
255 | }
256 |
257 | .gmt-main-icon-7 {
258 | -webkit-animation: animate-svg-stroke-7 1s ease-in 0.72s both,
259 | animate-svg-fill-7 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.4000000000000001s both;
260 | animation: animate-svg-stroke-7 1s ease-in 0.72s both,
261 | animate-svg-fill-7 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.4000000000000001s both;
262 | }
263 |
264 | @-webkit-keyframes animate-svg-stroke-8 {
265 | 0% {
266 | stroke-dashoffset: 328px;
267 | stroke-dasharray: 328px;
268 | }
269 |
270 | 100% {
271 | stroke-dashoffset: 0;
272 | stroke-dasharray: 328px;
273 | }
274 | }
275 |
276 | @keyframes animate-svg-stroke-8 {
277 | 0% {
278 | stroke-dashoffset: 328px;
279 | stroke-dasharray: 328px;
280 | }
281 |
282 | 100% {
283 | stroke-dashoffset: 0;
284 | stroke-dasharray: 328px;
285 | }
286 | }
287 |
288 | .gmt-main-icon-8 {
289 | -webkit-animation: animate-svg-stroke-8 1s ease-in 0.84s both,
290 | animate-svg-fill-8 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.5s both;
291 | animation: animate-svg-stroke-8 1s ease-in 0.84s both,
292 | animate-svg-fill-8 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.5s both;
293 | }
294 |
295 | @-webkit-keyframes animate-svg-stroke-9 {
296 | 0% {
297 | stroke-dashoffset: 1005.3450317382812px;
298 | stroke-dasharray: 1005.3450317382812px;
299 | }
300 |
301 | 100% {
302 | stroke-dashoffset: 0;
303 | stroke-dasharray: 1005.3450317382812px;
304 | }
305 | }
306 |
307 | @keyframes animate-svg-stroke-9 {
308 | 0% {
309 | stroke-dashoffset: 1005.3450317382812px;
310 | stroke-dasharray: 1005.3450317382812px;
311 | }
312 |
313 | 100% {
314 | stroke-dashoffset: 0;
315 | stroke-dasharray: 1005.3450317382812px;
316 | }
317 | }
318 |
319 | @-webkit-keyframes animate-svg-fill-9 {
320 | 0% {
321 | fill: transparent;
322 | }
323 |
324 | 100% {
325 | fill: rgb(0, 0, 0);
326 | }
327 | }
328 |
329 | @keyframes animate-svg-fill-9 {
330 | 0% {
331 | fill: transparent;
332 | }
333 |
334 | 100% {
335 | fill: rgb(0, 0, 0);
336 | }
337 | }
338 |
339 | .gmt-main-icon-9 {
340 | -webkit-animation: animate-svg-stroke-9 1s ease-in 0.96s both,
341 | animate-svg-fill-9 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.6s both;
342 | animation: animate-svg-stroke-9 1s ease-in 0.96s both,
343 | animate-svg-fill-9 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.6s both;
344 | }
--------------------------------------------------------------------------------
/gmt/templates/writers/settings.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Settings{% endblock %}
3 | {% block body %}
4 |
16 |
17 |
18 | Profile Settings:
19 |
20 |
21 | Update your profile settings here, these will be used to writers display your profile on the site.
22 |
23 | {% if status %}
24 |
26 | {{ status }}
27 |
28 | {% endif %}
29 |
30 |
182 |
183 | {% endblock %}
184 |
--------------------------------------------------------------------------------
/gmt/templates/writers/apply.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Apply as writer{% endblock %}
3 | {% block head %}
4 |
6 |
8 |
10 |
11 |
12 |
14 |
15 |
17 |
19 |
20 |
21 | {% endblock %}
22 | {% block body %}
23 |
24 |
25 |
26 |
27 | Step: 1 of 3
28 |
29 | Good Morning Tech Writer Application Form:
31 |
32 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {% if status %}
44 |
46 | {{ status }}
47 |
48 | {% endif %}
49 |
174 |
175 |
176 |
177 |
178 | {% endblock %}
179 |
--------------------------------------------------------------------------------
/gmt/templates/writers/register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Register{% endblock %}
3 | {% block body %}
4 |
5 |
6 |
7 |
8 |
9 | Step: 1 of 3
10 |
11 | Creating your Profile:
13 |
14 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% if status %}
26 |
28 | {{ status }}
29 |
30 | {% endif %}
31 |
190 |
191 |
192 |
193 |
194 | {% endblock %}
195 |
--------------------------------------------------------------------------------