├── api ├── __init__.py ├── routers │ ├── __init__.py │ ├── api.py │ └── healthcheck.py ├── tests │ ├── __init__.py │ └── routes │ │ ├── __init__.py │ │ └── test_healthcheck.py ├── migrations │ ├── README │ ├── script.py.mako │ ├── versions │ │ ├── 20210305_151900_update_comments_model.py │ │ ├── 20210322_211305_create_lat_and_lng_on_meetups.py │ │ ├── 20210312_133251_add_lat_and_lng_columns_to_retailer.py │ │ └── 20201120_150602_create_users_table.py │ └── env.py ├── models │ ├── __init__.py │ └── user.py ├── constants.py ├── README.org ├── main.py ├── database.py ├── alembic.ini └── settings.py ├── .flaskenv ├── app ├── schemas │ ├── __init__.py │ ├── config.py │ ├── community.py │ ├── tag.py │ ├── meetup.py │ ├── post.py │ ├── responses.py │ ├── comment.py │ ├── retailer.py │ └── user.py ├── tests │ ├── __init__.py │ └── models │ │ ├── __init__.py │ │ └── test_user.py ├── integration_tests │ └── routes │ │ ├── __init__.py │ │ ├── test_posts.py │ │ ├── test_comments.py │ │ └── test_auth.py ├── models │ ├── db.py │ ├── posts_tag.py │ ├── saved_post.py │ ├── saved_comment.py │ ├── thread.py │ ├── tag.py │ ├── post_image.py │ ├── retailer_image.py │ ├── __init__.py │ ├── community.py │ ├── comment_rating.py │ ├── message.py │ ├── post_rating.py │ ├── retailer_rating.py │ ├── meetup.py │ ├── retailer.py │ ├── user.py │ ├── comment.py │ └── post.py ├── forms │ ├── create_retailer_rating.py │ ├── create_post.py │ ├── create_comment.py │ ├── create_post_rating.py │ ├── create_comment_rating.py │ ├── create_retailer.py │ ├── create_community.py │ ├── __init__.py │ ├── create_meetup.py │ ├── login.py │ └── signup.py ├── routes │ ├── api │ │ ├── posts_image.py │ │ ├── lat_long.py │ │ ├── __init__.py │ │ ├── comment.py │ │ ├── user.py │ │ ├── auth.py │ │ └── meetup.py │ └── __init__.py ├── seeds │ ├── posts.py │ ├── threads.py │ ├── meetups.py │ ├── comments.py │ ├── post_images.py │ ├── retailers.py │ ├── post_ratings.py │ ├── communities.py │ ├── comment_ratings.py │ ├── retailer_ratings.py │ ├── users.py │ ├── post_images.json │ └── __init__.py ├── __init__.py ├── conftest.py ├── config.py └── helpers.py ├── react-app ├── .nvmrc ├── .prettierignore ├── src │ ├── setupTests.js │ ├── components │ │ ├── Map │ │ │ ├── index.js │ │ │ └── Map.js │ │ ├── About │ │ │ ├── index.js │ │ │ └── About.js │ │ ├── Post │ │ │ └── index.js │ │ ├── Meetup │ │ │ ├── index.js │ │ │ └── Meetup.js │ │ ├── NavBar │ │ │ └── index.js │ │ ├── Comment │ │ │ └── index.js │ │ ├── Sidebar │ │ │ ├── index.js │ │ │ └── Sidebar.js │ │ ├── parts │ │ │ ├── Score │ │ │ │ ├── index.js │ │ │ │ └── Score.js │ │ │ ├── Upvote │ │ │ │ ├── index.js │ │ │ │ └── Upvote.js │ │ │ ├── DivCard │ │ │ │ ├── index.js │ │ │ │ └── DivCard.js │ │ │ ├── Downvote │ │ │ │ ├── index.js │ │ │ │ └── Downvote.js │ │ │ ├── UserMenu │ │ │ │ ├── index.js │ │ │ │ └── UserMenu.js │ │ │ ├── UserName │ │ │ │ ├── index.js │ │ │ │ └── UserName.js │ │ │ ├── FormTitle │ │ │ │ ├── index.js │ │ │ │ └── FormTitle.js │ │ │ ├── NavButton │ │ │ │ ├── index.js │ │ │ │ └── NavButton.js │ │ │ ├── EditButton │ │ │ │ ├── index.js │ │ │ │ └── EditButton.js │ │ │ ├── FormErrors │ │ │ │ ├── index.js │ │ │ │ └── FormErrors.js │ │ │ ├── InputField │ │ │ │ ├── index.js │ │ │ │ └── InputField.js │ │ │ ├── SaveButton │ │ │ │ ├── index.js │ │ │ │ └── SaveButton.js │ │ │ ├── DeleteButton │ │ │ │ ├── index.js │ │ │ │ └── DeleteButton.js │ │ │ ├── SubmitFormButton │ │ │ │ ├── index.js │ │ │ │ └── SubmitFormButton.js │ │ │ └── DeleteConfirmation │ │ │ │ ├── index.js │ │ │ │ └── DeleteConfirmation.js │ │ ├── PostPage │ │ │ ├── index.js │ │ │ └── PostPage.js │ │ ├── Retailer │ │ │ ├── index.js │ │ │ └── Retailer.js │ │ ├── UserCard │ │ │ ├── index.js │ │ │ └── UserCard.js │ │ ├── Community │ │ │ ├── index.js │ │ │ └── Community.js │ │ ├── MeetupPage │ │ │ ├── index.js │ │ │ └── MeetupPage.js │ │ ├── SearchBar │ │ │ ├── index.js │ │ │ └── SearchBar.js │ │ ├── ProfilePage │ │ │ └── index.js │ │ ├── AdvSearchBar │ │ │ └── index.js │ │ ├── CreateMeetup │ │ │ └── index.js │ │ ├── LogoutButton │ │ │ ├── index.js │ │ │ └── LogoutButton.js │ │ ├── PageNotFound │ │ │ ├── index.js │ │ │ └── PageNotFound.js │ │ ├── RetailerPage │ │ │ ├── index.js │ │ │ └── RetailerPage.js │ │ ├── UserNotFound │ │ │ ├── index.js │ │ │ └── UserNotFound.js │ │ ├── SearchResults │ │ │ └── index.js │ │ ├── DarkModeToggle │ │ │ ├── index.js │ │ │ └── DarkModeToggle.js │ │ ├── PostsContainer │ │ │ └── index.js │ │ ├── RetailerRating │ │ │ ├── index.js │ │ │ └── RetailerRating.js │ │ ├── CollpasedSidebar │ │ │ └── index.js │ │ ├── MeetupsContainer │ │ │ ├── index.js │ │ │ └── MeetupsContainer.js │ │ ├── CreateCommentForm │ │ │ ├── index.js │ │ │ └── CreateCommentForm.js │ │ ├── RetailerRatingForm │ │ │ ├── index.js │ │ │ └── RetailerRatingForm.js │ │ ├── RetailersContainer │ │ │ ├── index.js │ │ │ └── RetailersContainer.js │ │ ├── CommentTreeContainer │ │ │ ├── index.js │ │ │ └── CommentTreeContainer.js │ │ ├── CommunitiesContainer │ │ │ ├── index.js │ │ │ └── CommunitiesContainer.js │ │ ├── CreateCommunityForm │ │ │ ├── index.js │ │ │ └── CreateCommunityForm.js │ │ ├── CommentThreadContainer │ │ │ ├── index.js │ │ │ └── CommentThreadContainer.js │ │ ├── RetailerRatingsContainer │ │ │ ├── index.js │ │ │ └── RetailerRatingsContainer.js │ │ ├── EditCommentForm │ │ │ ├── index.js │ │ │ └── EditCommentForm.js │ │ ├── EditPostForm │ │ │ └── index.js │ │ ├── LoginForm │ │ │ └── index.js │ │ ├── EditRetailerRatingForm │ │ │ ├── index.js │ │ │ └── EditRetailerRatingForm.js │ │ ├── SignUpForm │ │ │ └── index.js │ │ ├── EditMeetupForm │ │ │ └── index.js │ │ ├── CreatePostForm │ │ │ └── index.js │ │ └── __tests__ │ │ │ └── Post.js │ ├── images │ │ └── logo.png │ ├── utils │ │ ├── convertFormErrors.js │ │ └── localeDateString.js │ ├── index.css │ ├── context │ │ ├── CommentContext.js │ │ ├── DarkModeContext.js │ │ ├── CreatePostContext.js │ │ ├── RetailerRatingContext.js │ │ ├── SearchContext.js │ │ ├── CollapsedSidebarContext.js │ │ ├── AuthContext.js │ │ ├── index.js │ │ └── ModalContext.js │ ├── store │ │ ├── index.js │ │ ├── api.js │ │ ├── sidebar.js │ │ ├── search.js │ │ ├── constants.js │ │ ├── users.js │ │ ├── selectors.js │ │ └── communities.js │ └── index.js ├── global-setup.js ├── public │ ├── logo.png │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── manifest.json │ └── index.html ├── .prettierrc.json ├── postcss.config.js ├── .gitignore ├── .babelrc.js ├── tailwind.config.js └── package.json ├── .python-version ├── migrations ├── README ├── script.py.mako ├── versions │ ├── 20210305_151900_update_comments_model.py │ ├── 20210322_211305_create_lat_and_lng_on_meetups.py │ ├── 20210312_133251_add_lat_and_lng_columns_to_retailer.py │ └── 20201120_150602_create_users_table.py └── alembic.ini ├── .gitignore ├── .dockerignore ├── pyproject.toml ├── pytest.ini ├── .env.example ├── setup.cfg ├── .test.env.example ├── docker-compose.org ├── .github └── workflows │ ├── test-frontend.yml │ ├── pre-commit.yml │ ├── build.yml │ └── pytest.yml ├── docker-compose.yml ├── Dockerfile ├── dev-requirements.txt ├── .pre-commit-config.yaml ├── Pipfile ├── README.md ├── requirements.txt └── STARTER-README.md /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app -------------------------------------------------------------------------------- /api/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-app/.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11.2 2 | -------------------------------------------------------------------------------- /api/tests/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/integration_tests/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/integration_tests/routes/test_posts.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/integration_tests/routes/test_comments.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-app/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. 2 | -------------------------------------------------------------------------------- /api/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. 2 | -------------------------------------------------------------------------------- /react-app/src/setupTests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vim 2 | *.env* 3 | __pycache__/ 4 | *.py[cod] 5 | .venv 6 | .DS_Store 7 | *.db 8 | -------------------------------------------------------------------------------- /react-app/global-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = 'UTC'; 3 | }; 4 | -------------------------------------------------------------------------------- /react-app/src/components/Map/index.js: -------------------------------------------------------------------------------- 1 | import Map from './Map'; 2 | 3 | export default Map; 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /react-app/src/components/About/index.js: -------------------------------------------------------------------------------- 1 | import About from './About'; 2 | 3 | export default About; 4 | -------------------------------------------------------------------------------- /react-app/src/components/Post/index.js: -------------------------------------------------------------------------------- 1 | import Post from './Post'; 2 | 3 | export default Post; 4 | -------------------------------------------------------------------------------- /react-app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/logo.png -------------------------------------------------------------------------------- /react-app/src/components/Meetup/index.js: -------------------------------------------------------------------------------- 1 | import Meetup from './Meetup'; 2 | 3 | export default Meetup; 4 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import NavBar from './NavBar'; 2 | 3 | export default NavBar; 4 | -------------------------------------------------------------------------------- /api/models/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: F401 2 | from api.database import Base 3 | from api.models.user import User 4 | -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/src/components/Comment/index.js: -------------------------------------------------------------------------------- 1 | import Comment from './Comment'; 2 | 3 | export default Comment; 4 | -------------------------------------------------------------------------------- /react-app/src/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import Sidebar from './Sidebar'; 2 | 3 | export default Sidebar; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/Score/index.js: -------------------------------------------------------------------------------- 1 | import Score from './Score'; 2 | 3 | export default Score; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/Upvote/index.js: -------------------------------------------------------------------------------- 1 | import Upvote from './Upvote'; 2 | 3 | export default Upvote; 4 | -------------------------------------------------------------------------------- /react-app/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/src/images/logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | extend-exclude = 'migrations/versions' 3 | 4 | [tool.isort] 5 | profile = 'black' 6 | -------------------------------------------------------------------------------- /react-app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /react-app/src/components/PostPage/index.js: -------------------------------------------------------------------------------- 1 | import PostPage from './PostPage'; 2 | 3 | export default PostPage; 4 | -------------------------------------------------------------------------------- /react-app/src/components/Retailer/index.js: -------------------------------------------------------------------------------- 1 | import Retailer from './Retailer'; 2 | 3 | export default Retailer; 4 | -------------------------------------------------------------------------------- /react-app/src/components/UserCard/index.js: -------------------------------------------------------------------------------- 1 | import UserCard from './UserCard'; 2 | 3 | export default UserCard; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/DivCard/index.js: -------------------------------------------------------------------------------- 1 | import DivCard from './DivCard'; 2 | 3 | export default DivCard; 4 | -------------------------------------------------------------------------------- /react-app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/favicon-16x16.png -------------------------------------------------------------------------------- /react-app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/favicon-32x32.png -------------------------------------------------------------------------------- /react-app/src/components/Community/index.js: -------------------------------------------------------------------------------- 1 | import Community from './Community'; 2 | 3 | export default Community; 4 | -------------------------------------------------------------------------------- /react-app/src/components/MeetupPage/index.js: -------------------------------------------------------------------------------- 1 | import MeetupPage from './MeetupPage'; 2 | 3 | export default MeetupPage; 4 | -------------------------------------------------------------------------------- /react-app/src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | import SearchBar from './SearchBar'; 2 | 3 | export default SearchBar; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/Downvote/index.js: -------------------------------------------------------------------------------- 1 | import Downvote from './Downvote'; 2 | 3 | export default Downvote; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/UserMenu/index.js: -------------------------------------------------------------------------------- 1 | import UserMenu from './UserMenu'; 2 | 3 | export default UserMenu; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/UserName/index.js: -------------------------------------------------------------------------------- 1 | import UserName from './UserName'; 2 | 3 | export default UserName; 4 | -------------------------------------------------------------------------------- /react-app/src/components/ProfilePage/index.js: -------------------------------------------------------------------------------- 1 | import ProfilePage from './ProfilePage'; 2 | 3 | export default ProfilePage; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/FormTitle/index.js: -------------------------------------------------------------------------------- 1 | import FormTitle from './FormTitle'; 2 | 3 | export default FormTitle; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/NavButton/index.js: -------------------------------------------------------------------------------- 1 | import NavButton from './NavButton'; 2 | 3 | export default NavButton; 4 | -------------------------------------------------------------------------------- /react-app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /react-app/src/components/AdvSearchBar/index.js: -------------------------------------------------------------------------------- 1 | import AdvSearchBar from './AdvSearchBar'; 2 | 3 | export default AdvSearchBar; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CreateMeetup/index.js: -------------------------------------------------------------------------------- 1 | import CreateMeetup from './CreateMeetup'; 2 | 3 | export default CreateMeetup; 4 | -------------------------------------------------------------------------------- /react-app/src/components/LogoutButton/index.js: -------------------------------------------------------------------------------- 1 | import LogoutButton from './LogoutButton'; 2 | 3 | export default LogoutButton; 4 | -------------------------------------------------------------------------------- /react-app/src/components/PageNotFound/index.js: -------------------------------------------------------------------------------- 1 | import PageNotFound from './PageNotFound'; 2 | 3 | export default PageNotFound; 4 | -------------------------------------------------------------------------------- /react-app/src/components/RetailerPage/index.js: -------------------------------------------------------------------------------- 1 | import RetailerPage from './RetailerPage'; 2 | 3 | export default RetailerPage; 4 | -------------------------------------------------------------------------------- /react-app/src/components/UserNotFound/index.js: -------------------------------------------------------------------------------- 1 | import UserNotFound from './UserNotFound'; 2 | 3 | export default UserNotFound; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/EditButton/index.js: -------------------------------------------------------------------------------- 1 | import EditButton from './EditButton'; 2 | 3 | export default EditButton; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/FormErrors/index.js: -------------------------------------------------------------------------------- 1 | import FormErrors from './FormErrors'; 2 | 3 | export default FormErrors; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/InputField/index.js: -------------------------------------------------------------------------------- 1 | import InputField from './InputField'; 2 | 3 | export default InputField; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/SaveButton/index.js: -------------------------------------------------------------------------------- 1 | import SaveButton from './SaveButton'; 2 | 3 | export default SaveButton; 4 | -------------------------------------------------------------------------------- /api/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Tags(str, Enum): 5 | api = "API" 6 | healthcheck = "HEALTHCHECK" 7 | -------------------------------------------------------------------------------- /react-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /react-app/src/components/SearchResults/index.js: -------------------------------------------------------------------------------- 1 | import SearchResults from './SearchResults'; 2 | 3 | export default SearchResults; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/DeleteButton/index.js: -------------------------------------------------------------------------------- 1 | import DeleteButton from './DeleteButton'; 2 | 3 | export default DeleteButton; 4 | -------------------------------------------------------------------------------- /react-app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /react-app/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/Qwerkey/HEAD/react-app/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /react-app/src/components/DarkModeToggle/index.js: -------------------------------------------------------------------------------- 1 | import DarkModeToggle from './DarkModeToggle'; 2 | 3 | export default DarkModeToggle; 4 | -------------------------------------------------------------------------------- /react-app/src/components/PostsContainer/index.js: -------------------------------------------------------------------------------- 1 | import PostsContainer from './PostsContainer'; 2 | 3 | export default PostsContainer; 4 | -------------------------------------------------------------------------------- /react-app/src/components/RetailerRating/index.js: -------------------------------------------------------------------------------- 1 | import RetailerRating from './RetailerRating'; 2 | 3 | export default RetailerRating; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CollpasedSidebar/index.js: -------------------------------------------------------------------------------- 1 | import CollapsedSidebar from './CollapsedSidebar'; 2 | 3 | export default CollapsedSidebar; 4 | -------------------------------------------------------------------------------- /react-app/src/components/MeetupsContainer/index.js: -------------------------------------------------------------------------------- 1 | import MeetupsContainer from './MeetupsContainer'; 2 | 3 | export default MeetupsContainer; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CreateCommentForm/index.js: -------------------------------------------------------------------------------- 1 | import CreateCommentForm from './CreateCommentForm'; 2 | 3 | export default CreateCommentForm; 4 | -------------------------------------------------------------------------------- /react-app/src/components/parts/SubmitFormButton/index.js: -------------------------------------------------------------------------------- 1 | import SubmitFormButton from './SubmitFormButton'; 2 | 3 | export default SubmitFormButton; 4 | -------------------------------------------------------------------------------- /app/schemas/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class BaseORMModeModel(BaseModel): 5 | class Config: 6 | orm_mode = True 7 | -------------------------------------------------------------------------------- /react-app/src/components/RetailerRatingForm/index.js: -------------------------------------------------------------------------------- 1 | import RetailerRatingForm from './RetailerRatingForm'; 2 | 3 | export default RetailerRatingForm; 4 | -------------------------------------------------------------------------------- /react-app/src/components/RetailersContainer/index.js: -------------------------------------------------------------------------------- 1 | import RetailersContainer from './RetailersContainer'; 2 | 3 | export default RetailersContainer; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CommentTreeContainer/index.js: -------------------------------------------------------------------------------- 1 | import CommentTreeContainer from './CommentTreeContainer'; 2 | 3 | export default CommentTreeContainer; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CommunitiesContainer/index.js: -------------------------------------------------------------------------------- 1 | import CommunitiesContainer from './CommunitiesContainer'; 2 | 3 | export default CommunitiesContainer; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CreateCommunityForm/index.js: -------------------------------------------------------------------------------- 1 | import CreateCommunityForm from './CreateCommunityForm'; 2 | 3 | export default CreateCommunityForm; 4 | -------------------------------------------------------------------------------- /react-app/src/components/CommentThreadContainer/index.js: -------------------------------------------------------------------------------- 1 | import CommentThreadContainer from './CommentThreadContainer'; 2 | 3 | export default CommentThreadContainer; 4 | -------------------------------------------------------------------------------- /app/schemas/community.py: -------------------------------------------------------------------------------- 1 | from app.schemas.config import BaseORMModeModel 2 | 3 | 4 | class MinimalCommunityResponse(BaseORMModeModel): 5 | id: int 6 | name: str 7 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env_override_existing_values = 1 3 | env_files = 4 | .test.env 5 | testpaths = 6 | app/tests 7 | app/integration_tests 8 | api/tests 9 | -------------------------------------------------------------------------------- /react-app/src/components/RetailerRatingsContainer/index.js: -------------------------------------------------------------------------------- 1 | import RetailerRatingsContainer from './RetailerRatingsContainer'; 2 | 3 | export default RetailerRatingsContainer; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY= 4 | DATABASE_URL=postgresql://starter_app_dev@localhost/starter_app 5 | S3_BUCKET_NAME= 6 | S3_ACCESS_KEY= 7 | S3_SECRET_ACCESS_KEY= 8 | OPEN_CAGE_API_KEY= 9 | -------------------------------------------------------------------------------- /api/routers/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from api.constants import Tags 4 | from api.routers import healthcheck 5 | 6 | router = APIRouter(prefix="/api", tags=[Tags.api]) 7 | 8 | router.include_router(healthcheck.router) 9 | -------------------------------------------------------------------------------- /react-app/src/components/PageNotFound/PageNotFound.js: -------------------------------------------------------------------------------- 1 | const PageNotFound = () => { 2 | return ( 3 | <> 4 |

How'd you get here? Did you have a typo?

5 | 6 | ); 7 | }; 8 | 9 | export default PageNotFound; 10 | -------------------------------------------------------------------------------- /react-app/src/components/parts/FormTitle/FormTitle.js: -------------------------------------------------------------------------------- 1 | const FormTitle = ({ title }) => { 2 | return ( 3 |
4 |

{title}

5 |
6 | ); 7 | }; 8 | 9 | export default FormTitle; 10 | -------------------------------------------------------------------------------- /app/schemas/tag.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from app.schemas.config import BaseORMModeModel 4 | 5 | 6 | class MinimalTagResponse(BaseORMModeModel): 7 | id: int 8 | name: str 9 | description: str 10 | created_at: datetime 11 | -------------------------------------------------------------------------------- /react-app/src/components/UserNotFound/UserNotFound.js: -------------------------------------------------------------------------------- 1 | const UserNotFound = () => { 2 | return ( 3 |
4 |

User Not Found.

5 |
6 | ); 7 | }; 8 | 9 | export default UserNotFound; 10 | -------------------------------------------------------------------------------- /react-app/src/components/CommentTreeContainer/CommentTreeContainer.js: -------------------------------------------------------------------------------- 1 | const CommentTreeContainer = () => { 2 | return ( 3 | <> 4 |

Comment Tree Container Placeholder

5 | 6 | ); 7 | }; 8 | 9 | export default CommentTreeContainer; 10 | -------------------------------------------------------------------------------- /app/models/posts_tag.py: -------------------------------------------------------------------------------- 1 | from app.models.db import db 2 | 3 | posts_tags = db.Table( 4 | "posts_tags", 5 | db.Model.metadata, 6 | db.Column("post_id", db.Integer, db.ForeignKey("posts.id")), 7 | db.Column("tag_id", db.Integer, db.ForeignKey("tags.id")), 8 | ) 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = 4 | */versions/* 5 | per-file-ignores = 6 | api/models/__init__.py:F401 7 | 8 | [tool:pytest] 9 | testpaths = 10 | tests 11 | integration_tests 12 | 13 | [pycodestyle] 14 | max-line-length = 88 15 | -------------------------------------------------------------------------------- /app/models/saved_post.py: -------------------------------------------------------------------------------- 1 | from app.models.db import db 2 | 3 | saved_posts = db.Table( 4 | "saved_posts", 5 | db.Model.metadata, 6 | db.Column("user_id", db.Integer, db.ForeignKey("users.id")), 7 | db.Column("post_id", db.Integer, db.ForeignKey("posts.id")), 8 | ) 9 | -------------------------------------------------------------------------------- /.test.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=postgresql://qwerkey_test_app:<>@localhost/qwerkey_test_db 5 | S3_BUCKET_NAME= 6 | S3_ACCESS_KEY= 7 | S3_SECRET_ACCESS_KEY= 8 | -------------------------------------------------------------------------------- /app/models/saved_comment.py: -------------------------------------------------------------------------------- 1 | from app.models.db import db 2 | 3 | saved_comments = db.Table( 4 | "saved_comments", 5 | db.Model.metadata, 6 | db.Column("user_id", db.Integer, db.ForeignKey("users.id"), nullable=False), 7 | db.Column("comment_id", db.Integer, db.ForeignKey("comments.id"), nullable=False), 8 | ) 9 | -------------------------------------------------------------------------------- /react-app/src/components/parts/Score/Score.js: -------------------------------------------------------------------------------- 1 | const Score = ({ ratings }) => { 2 | return ( 3 |
4 | {ratings 5 | ? Object.values(ratings).reduce((acc, { rating }) => acc + rating, 0) 6 | : '0'} 7 |
8 | ); 9 | }; 10 | 11 | export default Score; 12 | -------------------------------------------------------------------------------- /react-app/src/utils/convertFormErrors.js: -------------------------------------------------------------------------------- 1 | const convertFormErrors = (errorsObj) => { 2 | const errors = []; 3 | for (const field in errorsObj) { 4 | errors.push( 5 | `${field[0].toUpperCase() + field.slice(1)}: ${errorsObj[field]}` 6 | ); 7 | } 8 | return errors; 9 | }; 10 | 11 | export default convertFormErrors; 12 | -------------------------------------------------------------------------------- /react-app/src/components/parts/DivCard/DivCard.js: -------------------------------------------------------------------------------- 1 | const DivCard = ({ children }) => { 2 | return ( 3 |
4 | {children} 5 |
6 | ); 7 | }; 8 | 9 | export default DivCard; 10 | -------------------------------------------------------------------------------- /react-app/src/components/parts/UserName/UserName.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | 3 | const UserName = ({ username, link }) => { 4 | return ( 5 | <> 6 | 7 | {username} 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default UserName; 14 | -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Qwerkey", 3 | "name": "Qwerkey", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /api/routers/healthcheck.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from sqlalchemy.orm import Session 3 | 4 | from api.constants import Tags 5 | from api.database import get_db 6 | 7 | router = APIRouter(prefix="/healthcheck", tags=[Tags.healthcheck, Tags.api]) 8 | 9 | 10 | @router.get("/") 11 | def healthcheck(_db: Session = Depends(get_db)): 12 | return {"status": "ok"} 13 | -------------------------------------------------------------------------------- /react-app/src/utils/localeDateString.js: -------------------------------------------------------------------------------- 1 | const localeDateStringOptions = { 2 | weekday: 'long', 3 | year: 'numeric', 4 | month: 'long', 5 | day: 'numeric', 6 | hour12: true, 7 | hour: '2-digit', 8 | minute: '2-digit', 9 | second: '2-digit', 10 | }; 11 | 12 | export default function spreadOptionsForDateString() { 13 | return ['en-US', localeDateStringOptions]; 14 | } 15 | -------------------------------------------------------------------------------- /app/forms/create_retailer_rating.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField 3 | from wtforms.validators import DataRequired, NumberRange 4 | 5 | 6 | class CreateRetailerRating(FlaskForm): 7 | user_id = IntegerField(validators=[DataRequired()]) 8 | rating = IntegerField( 9 | validators=[NumberRange(min=1, max=5, message="Must be between 1 and 5")] 10 | ) 11 | -------------------------------------------------------------------------------- /api/tests/routes/test_healthcheck.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from api.main import app 4 | 5 | client = TestClient(app) 6 | 7 | 8 | class TestBasicAssertions: 9 | def test__main_application__healthcheck__response_200(self): 10 | response = client.get("/api/healthcheck") 11 | 12 | assert response.status_code == 200 13 | assert response.json() == {"status": "ok"} 14 | -------------------------------------------------------------------------------- /app/forms/create_post.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, StringField, TextAreaField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class CreatePost(FlaskForm): 7 | title = StringField(validators=[DataRequired()]) 8 | body = TextAreaField() 9 | user_id = IntegerField(validators=[DataRequired()]) 10 | community_id = IntegerField(validators=[DataRequired()]) 11 | -------------------------------------------------------------------------------- /react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /app/schemas/meetup.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from app.schemas.config import BaseORMModeModel 5 | 6 | 7 | class MinimalMeetupResponse(BaseORMModeModel): 8 | id: int 9 | name: str 10 | description: str 11 | city: str 12 | state: str 13 | lat: Optional[float] 14 | lng: Optional[float] 15 | date: datetime 16 | created_at: datetime 17 | updated_at: datetime 18 | -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | h1 { 7 | @apply text-5xl; 8 | } 9 | h2 { 10 | @apply text-4xl; 11 | } 12 | h3 { 13 | @apply text-3xl; 14 | } 15 | h4 { 16 | @apply text-2xl; 17 | } 18 | h5 { 19 | @apply text-xl; 20 | } 21 | h6 { 22 | @apply text-lg; 23 | } 24 | p { 25 | @apply text-base; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /react-app/src/components/parts/SubmitFormButton/SubmitFormButton.js: -------------------------------------------------------------------------------- 1 | const SubmitFormButton = ({ label }) => { 2 | return ( 3 |
4 | 10 |
11 | ); 12 | }; 13 | 14 | export default SubmitFormButton; 15 | -------------------------------------------------------------------------------- /app/forms/create_comment.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, StringField, TextAreaField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class CreateComment(FlaskForm): 7 | body = TextAreaField(validators=[DataRequired()]) 8 | user_id = IntegerField(validators=[DataRequired()]) 9 | thread_id = IntegerField() 10 | comment_id = IntegerField() 11 | path = StringField() 12 | level = StringField() 13 | -------------------------------------------------------------------------------- /react-app/src/components/parts/NavButton/NavButton.js: -------------------------------------------------------------------------------- 1 | const NavButton = ({ name, onClick, children }) => { 2 | return ( 3 | <> 4 | 10 | {children} 11 | 12 | ); 13 | }; 14 | 15 | export default NavButton; 16 | -------------------------------------------------------------------------------- /app/forms/create_post_rating.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField 3 | from wtforms.validators import DataRequired, ValidationError 4 | 5 | 6 | def valid_rating(form, field): 7 | if field.data not in (-1, 0, 1): 8 | raise ValidationError("Rating required.") 9 | 10 | 11 | class CreatePostRating(FlaskForm): 12 | user_id = IntegerField(validators=[DataRequired()]) 13 | rating = IntegerField(validators=[valid_rating]) 14 | -------------------------------------------------------------------------------- /app/forms/create_comment_rating.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField 3 | from wtforms.validators import DataRequired, ValidationError 4 | 5 | 6 | def valid_rating(form, field): 7 | if field.data not in (-1, 0, 1): 8 | raise ValidationError("Rating required.") 9 | 10 | 11 | class CreateCommentRating(FlaskForm): 12 | user_id = IntegerField(validators=[DataRequired()]) 13 | rating = IntegerField(validators=[valid_rating]) 14 | -------------------------------------------------------------------------------- /app/routes/api/posts_image.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | from app.models import Post, PostsImage, db 4 | 5 | posts_image = Blueprint("posts_image", __name__) 6 | 7 | 8 | @posts_image.route("/", methods=["DELETE"]) 9 | def delete_posts_image(posts_image_id): 10 | image = PostsImage.query.get(posts_image_id) 11 | post = Post.query.get(image.post_id) 12 | db.session.delete(image) 13 | db.session.commit() 14 | return post.to_dict() 15 | -------------------------------------------------------------------------------- /app/schemas/post.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from app.schemas.config import BaseORMModeModel 4 | 5 | 6 | class MinimalPostImageResponse(BaseORMModeModel): 7 | id: int 8 | image_url: str 9 | created_at: datetime 10 | updated_at: datetime 11 | post_id: int 12 | 13 | 14 | class MinimalPostResponse(BaseORMModeModel): 15 | id: int 16 | title: str 17 | body: str 18 | images: list[MinimalPostImageResponse] 19 | created_at: datetime 20 | -------------------------------------------------------------------------------- /react-app/src/components/parts/DeleteButton/DeleteButton.js: -------------------------------------------------------------------------------- 1 | const DeleteButton = ({ className, label, onClick, children }) => { 2 | return ( 3 | <> 4 | 10 | {children} 11 | 12 | ); 13 | }; 14 | 15 | export default DeleteButton; 16 | -------------------------------------------------------------------------------- /react-app/src/components/parts/EditButton/EditButton.js: -------------------------------------------------------------------------------- 1 | const EditButton = ({ className, label, children, onClick }) => { 2 | return ( 3 | <> 4 | 10 | {children} 11 | 12 | ); 13 | }; 14 | 15 | export default EditButton; 16 | -------------------------------------------------------------------------------- /react-app/src/components/parts/SaveButton/SaveButton.js: -------------------------------------------------------------------------------- 1 | import { BookmarkBorder, Bookmark } from '@mui/icons-material'; 2 | 3 | const SaveButton = ({ save, isSaved }) => { 4 | return ( 5 | <> 6 | 12 | 13 | ); 14 | }; 15 | 16 | export default SaveButton; 17 | -------------------------------------------------------------------------------- /react-app/.babelrc.js: -------------------------------------------------------------------------------- 1 | const plugins = [ 2 | [ 3 | 'babel-plugin-import', 4 | { 5 | libraryName: '@material-ui/core', 6 | libraryDirectory: 'esm', 7 | camel2DashComponentName: false, 8 | }, 9 | 'core', 10 | ], 11 | [ 12 | 'babel-plugin-import', 13 | { 14 | libraryName: '@material-ui/icons', 15 | libraryDirectory: 'esm', 16 | camel2DashComponentName: false, 17 | }, 18 | 'icons', 19 | ], 20 | ]; 21 | 22 | module.exports = { plugins }; 23 | -------------------------------------------------------------------------------- /app/forms/create_retailer.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, StringField, TextAreaField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class CreateRetailer(FlaskForm): 7 | user_id = IntegerField(validators=[DataRequired()]) 8 | name = StringField(validators=[DataRequired()]) 9 | description = TextAreaField(validators=[DataRequired()]) 10 | city = StringField(validators=[DataRequired()]) 11 | state = StringField(validators=[DataRequired()]) 12 | -------------------------------------------------------------------------------- /api/README.org: -------------------------------------------------------------------------------- 1 | #+title: FastAPI Application 2 | 3 | An endeavor to migrate the existing Flask application over to FastAPI. 4 | 5 | ** Install Dependencies and Enter Pipenv 6 | #+begin_src shell 7 | % pipenv install --dev 8 | % pipenv shell 9 | #+end_src 10 | 11 | ** Running the application 12 | *** From the root of the project 13 | #+begin_src shell 14 | % uvicorn api.main:app --reload 15 | #+end_src 16 | *** From within the =api= directory 17 | #+begin_src shell 18 | % uvicorn main:app --reload 19 | #+end_src 20 | -------------------------------------------------------------------------------- /react-app/src/components/EditCommentForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import EditCommentForm from './EditCommentForm'; 3 | 4 | const EditCommentModal = ({ showEditModal, setShowEditModal }) => { 5 | return ( 6 | <> 7 | {showEditModal && ( 8 | setShowEditModal(false)}> 9 | 10 | 11 | )} 12 | 13 | ); 14 | }; 15 | 16 | export default EditCommentModal; 17 | -------------------------------------------------------------------------------- /app/seeds/posts.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import Post, db 4 | 5 | 6 | def seed_posts(): 7 | new_posts = [] 8 | with open("./app/seeds/posts.json") as f: 9 | data = json.load(f) 10 | for post in data: 11 | new_post = Post(**post) 12 | new_posts.append(new_post) 13 | 14 | db.session.add_all(new_posts) 15 | db.session.commit() 16 | 17 | 18 | def undo_posts(): 19 | db.session.execute("TRUNCATE posts RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /react-app/src/components/EditPostForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import EditPostForm from './EditPostForm'; 3 | 4 | const EditPostModal = ({ setShowEditModal, showEditModal, postId }) => { 5 | return ( 6 | <> 7 | {showEditModal && ( 8 | setShowEditModal(false)}> 9 | 10 | 11 | )} 12 | 13 | ); 14 | }; 15 | 16 | export default EditPostModal; 17 | -------------------------------------------------------------------------------- /react-app/src/context/CommentContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react'; 2 | 3 | const CommentContext = createContext(); 4 | 5 | export const useCommentContext = () => useContext(CommentContext); 6 | 7 | const CommentProvider = ({ children }) => { 8 | const [comment, setComment] = useState(); 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default CommentProvider; 18 | -------------------------------------------------------------------------------- /app/schemas/responses.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class BaseResponse(BaseModel): 5 | status: int = 200 6 | 7 | 8 | class BaseErrorsResponse(BaseResponse): 9 | errors: list[str] 10 | status: int = 400 11 | 12 | 13 | class UnauthenticatedErrorsResponse(BaseResponse): 14 | errors: list[str] = ["Unauthenticated"] 15 | status: int = 401 16 | message: str = "Not Authenticated" 17 | 18 | 19 | class LogoutResponse(BaseResponse): 20 | status: int = 200 21 | message: str = "User logged out" 22 | -------------------------------------------------------------------------------- /app/seeds/threads.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import Thread, db 4 | 5 | 6 | def seed_threads(): 7 | threads = [] 8 | with open("./app/seeds/threads.json") as f: 9 | data = json.load(f) 10 | for thread in data: 11 | new_thread = Thread(**thread) 12 | threads.append(new_thread) 13 | 14 | db.session.add_all(threads) 15 | db.session.commit() 16 | 17 | 18 | def undo_threads(): 19 | db.session.execute("TRUNCATE threads RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /app/seeds/meetups.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import Meetup, db 4 | 5 | 6 | def seed_meetups(): 7 | new_meetups = [] 8 | with open("./app/seeds/meetups.json") as f: 9 | data = json.load(f) 10 | for meetup in data: 11 | new_meetup = Meetup(**meetup) 12 | new_meetups.append(new_meetup) 13 | 14 | db.session.add_all(new_meetups) 15 | db.session.commit() 16 | 17 | 18 | def undo_meetups(): 19 | db.session.execute("TRUNCATE meetups RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /react-app/src/components/UserCard/UserCard.js: -------------------------------------------------------------------------------- 1 | import options from '../../utils/localeDateString'; 2 | 3 | const UserCard = ({ user }) => { 4 | return ( 5 |
6 |

{user.username}

7 |

8 | Account created on{' '} 9 | {new Date(user.created_at).toLocaleString(...options())} 10 |

11 |
12 | ); 13 | }; 14 | 15 | export default UserCard; 16 | -------------------------------------------------------------------------------- /react-app/src/components/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import { useAuthContext } from '../../context/AuthContext'; 3 | import LoginForm from './LoginForm'; 4 | 5 | const LoginModal = () => { 6 | const { showLoginModal, setShowLoginModal } = useAuthContext(); 7 | 8 | return ( 9 | <> 10 | {showLoginModal && ( 11 | setShowLoginModal(false)}> 12 | 13 | 14 | )} 15 | 16 | ); 17 | }; 18 | 19 | export default LoginModal; 20 | -------------------------------------------------------------------------------- /react-app/src/components/EditRetailerRatingForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import EditRetailerRatingForm from './EditRetailerRatingForm'; 3 | 4 | const EditRetailerRatingModal = ({ showEditModal, setShowEditModal }) => { 5 | return ( 6 | <> 7 | {showEditModal && ( 8 | setShowEditModal(false)}> 9 | 10 | 11 | )} 12 | 13 | ); 14 | }; 15 | 16 | export default EditRetailerRatingModal; 17 | -------------------------------------------------------------------------------- /app/seeds/comments.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import Comment, db 4 | 5 | 6 | def seed_comments(): 7 | new_comments = [] 8 | with open("./app/seeds/comments.json") as f: 9 | data = json.load(f) 10 | for comment in data: 11 | new_comment = Comment(**comment) 12 | new_comments.append(new_comment) 13 | 14 | db.session.add_all(new_comments) 15 | db.session.commit() 16 | 17 | 18 | def undo_comments(): 19 | db.session.execute("TRUNCATE comments RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /react-app/src/components/SignUpForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import { useAuthContext } from '../../context/AuthContext'; 3 | import SignUpForm from './SignUpForm'; 4 | 5 | const SignUpModal = () => { 6 | const { showSignUpModal, setShowSignUpModal } = useAuthContext(); 7 | 8 | return ( 9 | <> 10 | {showSignUpModal && ( 11 | setShowSignUpModal(false)}> 12 | 13 | 14 | )} 15 | 16 | ); 17 | }; 18 | 19 | export default SignUpModal; 20 | -------------------------------------------------------------------------------- /app/seeds/post_images.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import PostsImage, db 4 | 5 | 6 | def seed_post_images(): 7 | new_images = [] 8 | with open("./app/seeds/post_images.json") as f: 9 | data = json.load(f) 10 | for image in data: 11 | new_image = PostsImage(**image) 12 | new_images.append(new_image) 13 | 14 | db.session.add_all(new_images) 15 | db.session.commit() 16 | 17 | 18 | def undo_post_images(): 19 | db.session.execute("TRUNCATE posts_images RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /app/routes/api/lat_long.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from flask import Blueprint, request 3 | 4 | from app.config import Config 5 | 6 | lat_long = Blueprint("lat_long", __name__) 7 | 8 | 9 | @lat_long.route("/") 10 | def get_lat_long(): 11 | city = request.args.get("city") 12 | state = request.args.get("state") 13 | response = requests.get( 14 | "https://api.opencagedata.com/geocode/v1/json?" 15 | + f"key={Config.OPEN_CAGE_API_KEY}" 16 | + f"&q={city},{state},USA" 17 | ) 18 | data = response.json() 19 | return data["results"][0]["geometry"] 20 | -------------------------------------------------------------------------------- /app/seeds/retailers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import Retailer, db 4 | 5 | 6 | def seed_retailers(): 7 | new_retailers = [] 8 | with open("./app/seeds/retailers.json") as f: 9 | data = json.load(f) 10 | for retailer in data: 11 | new_retailer = Retailer(**retailer) 12 | new_retailers.append(new_retailer) 13 | 14 | db.session.add_all(new_retailers) 15 | db.session.commit() 16 | 17 | 18 | def undo_retailers(): 19 | db.session.execute("TRUNCATE retailers RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /app/seeds/post_ratings.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import PostRating, db 4 | 5 | 6 | def seed_post_ratings(): 7 | new_ratings = [] 8 | with open("./app/seeds/post_ratings.json") as f: 9 | data = json.load(f) 10 | for rating in data: 11 | new_rating = PostRating(**rating) 12 | new_ratings.append(new_rating) 13 | 14 | db.session.add_all(new_ratings) 15 | db.session.commit() 16 | 17 | 18 | def undo_post_ratings(): 19 | db.session.execute("TRUNCATE post_ratings RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /react-app/src/components/EditMeetupForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import EditMeetupForm from './EditMeetupForm'; 3 | 4 | const EditMeetupModal = ({ setShowEditModal, showEditModal, meetupId }) => { 5 | return ( 6 | <> 7 | {showEditModal && ( 8 | setShowEditModal(false)}> 9 | 13 | 14 | )} 15 | 16 | ); 17 | }; 18 | 19 | export default EditMeetupModal; 20 | -------------------------------------------------------------------------------- /app/seeds/communities.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import Community, db 4 | 5 | 6 | def seed_communities(): 7 | new_communities = [] 8 | with open("./app/seeds/communities.json") as f: 9 | data = json.load(f) 10 | for community in data: 11 | new_community = Community(**community) 12 | new_communities.append(new_community) 13 | 14 | db.session.add_all(new_communities) 15 | db.session.commit() 16 | 17 | 18 | def undo_communities(): 19 | db.session.execute("TRUNCATE communities RESTART IDENTITY CASCADE") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /app/seeds/comment_ratings.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import CommentRating, db 4 | 5 | 6 | def seed_comment_ratings(): 7 | new_ratings = [] 8 | with open("./app/seeds/comment_ratings.json") as f: 9 | data = json.load(f) 10 | for rating in data: 11 | new_rating = CommentRating(**rating) 12 | new_ratings.append(new_rating) 13 | 14 | db.session.add_all(new_ratings) 15 | db.session.commit() 16 | 17 | 18 | def undo_comment_ratings(): 19 | db.session.execute("TRUNCATE comment_ratings RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /app/seeds/retailer_ratings.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import RetailerRating, db 4 | 5 | 6 | def seed_retailer_ratings(): 7 | new_ratings = [] 8 | with open("./app/seeds/retailer_ratings.json") as f: 9 | data = json.load(f) 10 | for rating in data: 11 | new_rating = RetailerRating(**rating) 12 | new_ratings.append(new_rating) 13 | 14 | db.session.add_all(new_ratings) 15 | db.session.commit() 16 | 17 | 18 | def undo_retailer_ratings(): 19 | db.session.execute("TRUNCATE retailer_ratings RESTART IDENTITY CASCADE;") 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /react-app/src/context/DarkModeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react'; 2 | 3 | const DarkModeContext = createContext(); 4 | 5 | export const useDarkModeContext = () => useContext(DarkModeContext); 6 | 7 | const DarkModeProvider = ({ children }) => { 8 | const [isDarkMode, setIsDarkMode] = useState( 9 | localStorage.getItem('isDarkMode') === 'true' || false 10 | ); 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default DarkModeProvider; 20 | -------------------------------------------------------------------------------- /app/forms/create_community.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, TextAreaField 3 | from wtforms.validators import DataRequired, ValidationError 4 | 5 | from app.models import Community 6 | 7 | 8 | def community_exists(form, field): 9 | community = Community.query.filter(Community.name == field.data).first() 10 | if community: 11 | raise ValidationError("Community name already exists.") 12 | 13 | 14 | class CreateCommunity(FlaskForm): 15 | name = StringField(validators=[DataRequired(), community_exists]) 16 | description = TextAreaField(validators=[DataRequired()]) 17 | -------------------------------------------------------------------------------- /react-app/src/context/CreatePostContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useContext } from 'react'; 2 | 3 | const CreatePostContext = createContext(); 4 | 5 | export const useCreatePostContext = () => useContext(CreatePostContext); 6 | 7 | const CreatePostProvider = ({ children }) => { 8 | const [showCreatePostModal, setShowCreatePostModal] = useState(false); 9 | 10 | return ( 11 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default CreatePostProvider; 20 | -------------------------------------------------------------------------------- /api/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from api.routers import api 5 | from api.settings import settings 6 | 7 | app = FastAPI() 8 | 9 | origins = [ 10 | "http://localhost:3000", 11 | ] 12 | 13 | if settings.is_dev: 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_origins=origins, 17 | allow_credentials=True, 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | ) 21 | 22 | app.include_router(api.router) 23 | 24 | 25 | @app.get("/") 26 | async def root(): 27 | return {"message": "Hello World"} 28 | -------------------------------------------------------------------------------- /react-app/src/context/RetailerRatingContext.js: -------------------------------------------------------------------------------- 1 | import { useContext, useState, createContext } from 'react'; 2 | 3 | const RetailerRatingContext = createContext(); 4 | 5 | export const useRetailerRatingContext = () => useContext(RetailerRatingContext); 6 | 7 | const RetailerRatingProvider = ({ children }) => { 8 | const [retailerRating, setRetailerRating] = useState(); 9 | 10 | return ( 11 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default RetailerRatingProvider; 20 | -------------------------------------------------------------------------------- /react-app/src/components/Community/Community.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | 3 | const Community = ({ community }) => { 4 | return ( 5 |
6 |

7 | 8 | {community.name} 9 | 10 |

11 |

{community.description}

12 |
13 | ); 14 | }; 15 | 16 | export default Community; 17 | -------------------------------------------------------------------------------- /react-app/src/components/CreatePostForm/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../context/ModalContext'; 2 | import { useCreatePostContext } from '../../context/CreatePostContext'; 3 | import CreatePostForm from './CreatePostForm'; 4 | 5 | const CreatePostModal = () => { 6 | const { showCreatePostModal, setShowCreatePostModal } = 7 | useCreatePostContext(); 8 | 9 | return ( 10 | <> 11 | {showCreatePostModal && ( 12 | setShowCreatePostModal(false)}> 13 | 14 | 15 | )} 16 | 17 | ); 18 | }; 19 | 20 | export default CreatePostModal; 21 | -------------------------------------------------------------------------------- /react-app/src/context/SearchContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react'; 2 | 3 | const SearchContext = createContext(); 4 | 5 | export const useSearchContext = () => useContext(SearchContext); 6 | 7 | const SearchProvider = ({ children }) => { 8 | const [searchInput, setSearchInput] = useState(''); 9 | const [searched, setSearched] = useState(false); 10 | 11 | return ( 12 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export default SearchProvider; 21 | -------------------------------------------------------------------------------- /api/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from app.forms.create_comment import CreateComment # noqa 2 | from app.forms.create_comment_rating import CreateCommentRating # noqa 3 | from app.forms.create_community import CreateCommunity # noqa 4 | from app.forms.create_meetup import CreateMeetup # noqa 5 | from app.forms.create_post import CreatePost # noqa 6 | from app.forms.create_post_rating import CreatePostRating # noqa 7 | from app.forms.create_retailer import CreateRetailer # noqa 8 | from app.forms.create_retailer_rating import CreateRetailerRating # noqa 9 | from app.forms.login import LoginForm # noqa 10 | from app.forms.signup import SignUpForm # noqa 11 | -------------------------------------------------------------------------------- /docker-compose.org: -------------------------------------------------------------------------------- 1 | #+title: Docker-Compose 2 | 3 | ** Running the Images 4 | #+begin_src bash 5 | docker compose up 6 | #+end_src 7 | ** Set up Postgres Container 8 | #+begin_src bash 9 | docker compose exec postgres sh 10 | 11 | psql -U postgres 12 | #+end_src 13 | 14 | #+begin_src sql 15 | create user qwerkey_app with password 'notsecret'; 16 | 17 | create database qwerkey_app with owner qwerkey_app; 18 | #+end_src 19 | ** Apply Migrations 20 | #+begin_src bash 21 | docker compose exec server sh 22 | flask db upgrade 23 | #+end_src 24 | ** Seed the database 25 | #+begin_src bash 26 | docker compose exec server sh 27 | flask seed all 28 | #+end_src 29 | -------------------------------------------------------------------------------- /react-app/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | import session from './session'; 4 | import posts from './posts'; 5 | import comments from './comments'; 6 | import retailers from './retailers'; 7 | import search from './search'; 8 | import communities from './communities'; 9 | import sidebar from './sidebar'; 10 | import users from './users'; 11 | import meetups from './meetups'; 12 | 13 | export const store = configureStore({ 14 | reducer: { 15 | session, 16 | posts, 17 | comments, 18 | retailers, 19 | search, 20 | communities, 21 | sidebar, 22 | users, 23 | meetups, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /.github/workflows/test-frontend.yml: -------------------------------------------------------------------------------- 1 | name: Test Frontend 2 | 3 | on: 4 | pull_request: 5 | branches: [staging] 6 | paths: 7 | - "react-app/**" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | defaults: 14 | run: 15 | working-directory: react-app 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 14 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | cache: "npm" 24 | cache-dependency-path: react-app/package-lock.json 25 | # Not my best idea... 26 | - run: npm install --force 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: "Pre-Commit Check" 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | env: 9 | PYTHON_VERSION: 3.11 10 | jobs: 11 | pre-commit: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 20 14 | steps: 15 | - uses: actions/cache@v2 16 | with: 17 | path: ~/.cache/pre-commit 18 | key: pre-commit-${{ env.PYTHON_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }} 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.11" 23 | - uses: pre-commit/action@v2.0.2 24 | -------------------------------------------------------------------------------- /app/models/thread.py: -------------------------------------------------------------------------------- 1 | from app.models.db import db 2 | 3 | 4 | class Thread(db.Model): 5 | __tablename__ = "threads" 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False) 9 | 10 | comments = db.relationship("Comment", back_populates="thread") 11 | post = db.relationship("Post", back_populates="threads") 12 | 13 | def to_dict(self): 14 | return { 15 | "id": self.id, 16 | "comments": { 17 | comment.id: comment.to_simple_dict() for comment in self.comments 18 | }, 19 | "post_id": self.post_id, 20 | } 21 | -------------------------------------------------------------------------------- /react-app/src/context/CollapsedSidebarContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react'; 2 | 3 | const CollapsedSidebarContext = createContext(); 4 | 5 | export const useCollapsedSidebarContext = () => 6 | useContext(CollapsedSidebarContext); 7 | 8 | const CollapsedSidebarProvider = ({ children }) => { 9 | const [showCollapsedSidebar, setShowCollapsedSidebar] = useState(false); 10 | 11 | return ( 12 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export default CollapsedSidebarProvider; 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Test 2 | 3 | on: 4 | pull_request: 5 | branches: [staging] 6 | paths: 7 | - "react-app/**" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | defaults: 14 | run: 15 | working-directory: react-app 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 18 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | cache: "npm" 24 | cache-dependency-path: react-app/package-lock.json 25 | # This isn't my best idea... 26 | - run: npm install --force 27 | - run: npm run build --if-present 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | postgres-data: {} 3 | 4 | services: 5 | server: 6 | build: . 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - .:/usr/src/app 11 | environment: 12 | PORT: 8000 13 | SECRET_KEY: 481284c381c6e4b9a520739cfbb74fa755b9520467187a35cac9468e82fe603c 14 | DATABASE_URL: postgresql://qwerkey_app:notsecret@postgres:5432/qwerkey_app 15 | postgres: 16 | image: "postgres:alpine" 17 | environment: 18 | POSTGRES_DB: qwerkey_app 19 | POSTGRES_USER: qwerkey_app 20 | POSTGRES_PASSWORD: notsecret 21 | ports: 22 | - "5432:5432" 23 | volumes: 24 | - postgres-data:/var/lib/postgres/data 25 | -------------------------------------------------------------------------------- /app/schemas/comment.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from app.schemas.config import BaseORMModeModel 5 | 6 | 7 | class CommentRatingResponse(BaseORMModeModel): 8 | id: int 9 | comment_id: int 10 | rating: int 11 | 12 | 13 | class MinimalCommentResponse(BaseORMModeModel): 14 | id: int 15 | body: str 16 | comment_id: Optional[int] 17 | 18 | 19 | class SearchCommentResponse(MinimalCommentResponse): 20 | ratings: list[CommentRatingResponse] 21 | created_at: datetime 22 | 23 | 24 | class FullCommentResponse(SearchCommentResponse): 25 | thread_id: int 26 | path: str 27 | level: int 28 | updated_at: datetime 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 AS build-stage 2 | 3 | WORKDIR /react-app 4 | COPY react-app/package.json react-app/package-lock.json ./ 5 | 6 | RUN npm install 7 | 8 | COPY react-app/ ./ 9 | RUN npm run build 10 | 11 | FROM python:3.11 12 | 13 | # Setup Flask environment 14 | ENV FLASK_APP=app 15 | ENV FLASK_ENV=production 16 | ENV SQLALCHEMY_ECHO=True 17 | 18 | EXPOSE 8000 19 | 20 | WORKDIR /var/www 21 | COPY requirements.txt . 22 | 23 | # Install Python Dependencies 24 | RUN pip install -r requirements.txt 25 | RUN pip install psycopg2 26 | 27 | COPY . . 28 | COPY --from=build-stage /react-app/build/* app/static/ 29 | 30 | # Run flask environment 31 | CMD gunicorn --bind 0.0.0.0:$PORT 'app:create_app()' 32 | -------------------------------------------------------------------------------- /react-app/src/components/CommentThreadContainer/CommentThreadContainer.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | import { session, comments as commentsSelectors } from '../../store/selectors'; 4 | import Comment from '../Comment'; 5 | 6 | const CommentsContainer = () => { 7 | const user = useSelector(session.user()); 8 | const comments = useSelector(commentsSelectors.all()); 9 | 10 | return ( 11 | <> 12 | {Object.values(comments).map((comment) => ( 13 | 18 | ))} 19 | 20 | ); 21 | }; 22 | 23 | export default CommentsContainer; 24 | -------------------------------------------------------------------------------- /react-app/src/store/api.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | const defaultOptions = { headers: {} }; 4 | 5 | export default function api(url, options = defaultOptions, isFormData = false) { 6 | if (!isFormData) { 7 | return fetch(url, { 8 | method: 'GET', 9 | ...options, 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | 'X-CSRFToken': Cookies.get('csrf_token'), 13 | ...options.headers, 14 | }, 15 | }); 16 | } else { 17 | return fetch(url, { 18 | method: 'GET', 19 | ...options, 20 | headers: { 21 | 'X-CSRFToken': Cookies.get('csrf_token'), 22 | ...options.headers, 23 | }, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /react-app/src/components/parts/DeleteConfirmation/index.js: -------------------------------------------------------------------------------- 1 | import { Modal } from '../../../context/ModalContext'; 2 | import DeleteConfirmation from './DeleteConfirmation'; 3 | 4 | const DeleteConfirmationModal = ({ 5 | showDeleteModal, 6 | setShowDeleteModal, 7 | id, 8 | extraId, 9 | type, 10 | }) => { 11 | return ( 12 | <> 13 | {showDeleteModal && ( 14 | setShowDeleteModal(false)}> 15 | 21 | 22 | )} 23 | 24 | ); 25 | }; 26 | 27 | export default DeleteConfirmationModal; 28 | -------------------------------------------------------------------------------- /react-app/src/components/LogoutButton/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | 3 | import { logout } from '../../store/session'; 4 | import { useAuthContext } from '../../context/AuthContext'; 5 | 6 | const LogoutButton = () => { 7 | const dispatch = useDispatch(); 8 | const { setAuthenticated } = useAuthContext(); 9 | 10 | const onLogout = async () => { 11 | await dispatch(logout()); 12 | setAuthenticated(false); 13 | }; 14 | 15 | return ( 16 | 22 | ); 23 | }; 24 | 25 | export default LogoutButton; 26 | -------------------------------------------------------------------------------- /app/models/tag.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from app.models.db import db 4 | 5 | 6 | class Tag(db.Model): 7 | __tablename__ = "tags" 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | name = db.Column(db.String(50), nullable=False) 11 | description = db.Column(db.String(100), nullable=True) 12 | created_at = db.Column( 13 | db.DateTime, nullable=False, default=datetime.datetime.utcnow 14 | ) 15 | updated_at = db.Column( 16 | db.DateTime, nullable=False, default=datetime.datetime.utcnow 17 | ) 18 | 19 | def to_dict(self): 20 | return { 21 | "id": self.id, 22 | "name": self.name, 23 | "description": self.description, 24 | "created_at": self.created_at, 25 | } 26 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | anyio==3.7.0 ; python_version >= '3.7' 3 | astroid==2.15.5 ; python_full_version >= '3.7.2' 4 | autopep8==2.0.2 5 | certifi==2020.12.5 6 | dill==0.3.6 ; python_version >= '3.11' 7 | h11==0.14.0 ; python_version >= '3.7' 8 | httpcore==0.17.2 ; python_version >= '3.7' 9 | httpx==0.24.1 10 | idna==2.10 11 | isort==5.12.0 ; python_full_version >= '3.8.0' 12 | lazy-object-proxy==1.9.0 ; python_version >= '3.7' 13 | mccabe==0.7.0 ; python_version >= '3.6' 14 | platformdirs==3.8.0 ; python_version >= '3.7' 15 | psycopg2-binary==2.9.6 16 | pycodestyle==2.10.0 ; python_version >= '3.6' 17 | pylint==2.17.4 18 | sniffio==1.3.0 ; python_version >= '3.7' 19 | tomlkit==0.11.8 ; python_version >= '3.7' 20 | wrapt==1.15.0 ; python_version >= '3.11' 21 | -------------------------------------------------------------------------------- /app/schemas/retailer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from app.schemas.config import BaseORMModeModel 4 | 5 | 6 | class MinimalRetailerRatingResponse(BaseORMModeModel): 7 | id: int 8 | rating: int 9 | 10 | 11 | class FullRetailerRatingResponse(MinimalRetailerRatingResponse): 12 | retailer_id: int 13 | created_at: datetime 14 | 15 | 16 | class MinimalRetailerImageResponse: 17 | id: int 18 | retailer_id: int 19 | image_url: str 20 | created_at: datetime 21 | 22 | 23 | class MinimalRetailerResponse(BaseORMModeModel): 24 | id: int 25 | name: str 26 | description: str 27 | city: str 28 | state: str 29 | created_at: datetime 30 | 31 | 32 | class FullRetailerResponse(MinimalRetailerResponse): 33 | lat: float 34 | lng: float 35 | -------------------------------------------------------------------------------- /react-app/src/components/parts/FormErrors/FormErrors.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const FormErrors = ({ errors }) => { 4 | const [formattedErrors, setFormattedErrors] = useState([]); 5 | 6 | useEffect(() => { 7 | const result = errors.map((error) => { 8 | const arr = error.split(':')[0].trim(); 9 | return arr[0].toUpperCase() + arr.slice(1); 10 | }); 11 | setFormattedErrors(result); 12 | }, [errors]); 13 | 14 | return ( 15 | <> 16 | {errors.length > 0 && ( 17 |
18 |
The following fields are required:
19 | {formattedErrors.join(', ')} 20 |
21 | )} 22 | 23 | ); 24 | }; 25 | 26 | export default FormErrors; 27 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.models import User, db 4 | 5 | 6 | # Adds a demo user, you can add other users here if you want 7 | def seed_users(): 8 | new_users = [] 9 | with open("./app/seeds/users.json") as f: 10 | data = json.load(f) 11 | for user in data: 12 | new_user = User(**user) 13 | new_users.append(new_user) 14 | 15 | db.session.add_all(new_users) 16 | db.session.commit() 17 | 18 | 19 | # Uses a raw SQL query to TRUNCATE the users table. 20 | # SQLAlchemy doesn't have a built in function to do this 21 | # TRUNCATE Removes all the data from the table, and resets 22 | # the auto incrementing primary key 23 | def undo_users(): 24 | db.session.execute("TRUNCATE users RESTART IDENTITY CASCADE;") 25 | db.session.commit() 26 | -------------------------------------------------------------------------------- /react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | import './index.css'; 7 | import App from './App'; 8 | import { store } from './store'; 9 | import ContextProvider from './context'; 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | window.store = store; 13 | } 14 | 15 | const Root = () => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | const root = createRoot(document.getElementById('root')); 26 | 27 | root.render( 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /react-app/src/components/Map/Map.js: -------------------------------------------------------------------------------- 1 | import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'; 2 | 3 | const Map = ({ long, lat }) => { 4 | return ( 5 |
6 | 11 | 15 | 16 | 17 | A pretty CSS3 popup.
Easily customizable. 18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Map; 26 | -------------------------------------------------------------------------------- /react-app/src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useContext } from 'react'; 2 | 3 | const AuthContext = createContext(); 4 | 5 | export const useAuthContext = () => useContext(AuthContext); 6 | 7 | const AuthProvider = ({ children }) => { 8 | const [showLoginModal, setShowLoginModal] = useState(false); 9 | const [showSignUpModal, setShowSignUpModal] = useState(false); 10 | const [authenticated, setAuthenticated] = useState(false); 11 | 12 | return ( 13 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export default AuthProvider; 29 | -------------------------------------------------------------------------------- /api/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | from werkzeug.security import check_password_hash, generate_password_hash 4 | 5 | from api.database import Base 6 | 7 | 8 | class User(Base): 9 | __tablename__ = "users" 10 | 11 | username: Mapped[str] = mapped_column(String(40), unique=True) 12 | email: Mapped[str] = mapped_column(String(255), unique=True) 13 | hashed_password: Mapped[str] = mapped_column(String(255)) 14 | 15 | @property 16 | def password(self) -> str: 17 | return self.hashed_password 18 | 19 | @password.setter 20 | def password(self, password: str): 21 | self.hashed_password = generate_password_hash(password) 22 | 23 | def check_password(self, password: str) -> bool: 24 | return check_password_hash(self.password, password) 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.11 3 | node: 18.16.0 4 | repos: 5 | - repo: https://github.com/psf/black 6 | rev: 23.3.0 7 | hooks: 8 | - id: black 9 | language_version: python 10 | types: [python] 11 | exclude: /versions/ 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: 6.0.0 14 | hooks: 15 | - id: flake8 16 | - repo: https://github.com/PyCQA/isort 17 | rev: 5.12.0 18 | hooks: 19 | - id: isort 20 | - repo: https://github.com/pre-commit/mirrors-prettier 21 | rev: "v2.7.1" 22 | hooks: 23 | - id: prettier 24 | - repo: https://github.com/pre-commit/pre-commit-hooks 25 | rev: v4.1.0 26 | hooks: 27 | - id: no-commit-to-branch 28 | args: 29 | - --branch 30 | - staging 31 | - --branch 32 | - main 33 | -------------------------------------------------------------------------------- /app/forms/create_meetup.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, StringField, TextAreaField 3 | from wtforms.validators import DataRequired, ValidationError 4 | 5 | 6 | def city_not_default(form, field): 7 | if field.data == "City...": 8 | raise ValidationError("City is required.") 9 | 10 | 11 | def state_not_default(form, field): 12 | if field.data == "State...": 13 | raise ValidationError("State is required.") 14 | 15 | 16 | class CreateMeetup(FlaskForm): 17 | user_id = IntegerField(validators=[DataRequired()]) 18 | name = StringField(validators=[DataRequired()]) 19 | description = TextAreaField(validators=[DataRequired()]) 20 | city = StringField(validators=[DataRequired(), city_not_default]) 21 | state = StringField(validators=[DataRequired(), state_not_default]) 22 | date = StringField(validators=[DataRequired()]) 23 | -------------------------------------------------------------------------------- /app/models/post_image.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from app.models.db import db 4 | 5 | 6 | class PostsImage(db.Model): 7 | __tablename__ = "posts_images" 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False) 11 | image_url = db.Column(db.String, nullable=False) 12 | created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) 13 | updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) 14 | 15 | post = db.relationship("Post", back_populates="images") 16 | 17 | def to_dict(self): 18 | return { 19 | "id": self.id, 20 | "post_id": self.post_id, 21 | "image_url": self.image_url, 22 | "created_at": self.created_at, 23 | "updated_at": self.updated_at, 24 | "post": self.post.to_dict(), 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Pytest 2 | 3 | on: 4 | pull_request: 5 | branches: [staging] 6 | paths: 7 | - "app/**" 8 | - "api/**" 9 | - "Pipfile*" 10 | - "*requirements.txt" 11 | 12 | jobs: 13 | pytest: 14 | runs-on: ubuntu-latest 15 | env: 16 | SECRET_KEY: ${{ secrets.SECRET_KEY }} 17 | S3_BUCKET_NAME: test 18 | S3_ACCESS_KEY: test 19 | S3_SECRET_ACCESS_KEY: test 20 | DATABASE_URL: "postgresql://test:test@localhost/test" 21 | OPEN_CAGE_API_KEY: test 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-python@v3 26 | with: 27 | python-version: "3.11" 28 | - name: Install Pipenv 29 | run: pip install pipenv 30 | - name: Install dependencies 31 | run: pipenv install --deploy --dev 32 | - name: Run pytest 33 | run: pipenv run pytest 34 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | from flask_login import LoginManager 4 | from flask_migrate import Migrate 5 | from flask_wtf.csrf import CSRFProtect 6 | 7 | from app.config import Config 8 | from app.models import User, db 9 | from app.routes import routes 10 | from app.seeds import seed_commands 11 | 12 | 13 | def create_app(testing=False): 14 | app = Flask(__name__) 15 | 16 | login = LoginManager(app) 17 | login.login_view = "api.auth.unauthorized" 18 | 19 | config = Config(testing) 20 | app.config.from_object(config) 21 | 22 | @login.user_loader 23 | def load_user(id): 24 | return User.query.get(int(id)) 25 | 26 | app.cli.add_command(seed_commands) 27 | 28 | db.init_app(app) 29 | Migrate(app, db) 30 | CORS(app) 31 | CSRFProtect(app) 32 | 33 | app.register_blueprint(routes) 34 | 35 | return app 36 | -------------------------------------------------------------------------------- /react-app/src/components/RetailerRatingsContainer/RetailerRatingsContainer.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { retailers } from '../../store/selectors'; 5 | import RetailerRating from '../RetailerRating'; 6 | 7 | const RetailerRatingsContainer = ({ retailerId }) => { 8 | const retailer = useSelector(retailers.byId(retailerId)); 9 | 10 | const [isLoaded, setIsLoaded] = useState(false); 11 | 12 | useEffect(() => { 13 | if (retailer) { 14 | setIsLoaded(true); 15 | } 16 | }, [retailer]); 17 | 18 | if (!isLoaded) { 19 | return null; 20 | } 21 | 22 | return ( 23 |
24 | {Object.values(retailer.ratings).map((rating) => ( 25 | 26 | ))} 27 |
28 | ); 29 | }; 30 | 31 | export default RetailerRatingsContainer; 32 | -------------------------------------------------------------------------------- /api/database.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import BigInteger, create_engine 4 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker 5 | 6 | from api.settings import settings 7 | 8 | engine = create_engine( 9 | str(settings.database_url), echo=settings.sqlalchemy_settings.echo 10 | ) 11 | SessionLocal = sessionmaker( 12 | autocommit=settings.sqlalchemy_settings.autocommit, 13 | autoflush=settings.sqlalchemy_settings.autoflush, 14 | bind=engine, 15 | ) 16 | 17 | 18 | def get_db(): 19 | db = SessionLocal() 20 | try: 21 | yield db 22 | finally: 23 | db.close() 24 | 25 | 26 | class Base(DeclarativeBase): 27 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True) 28 | created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) 29 | updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) 30 | -------------------------------------------------------------------------------- /app/models/retailer_image.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from app.models.db import db 4 | 5 | 6 | class RetailerImage(db.Model): 7 | __tablename__ = "retailer_images" 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | retailer_id = db.Column(db.Integer, db.ForeignKey("retailers.id"), nullable=False) 11 | image_url = db.Column(db.String, nullable=False) 12 | created_at = db.Column( 13 | db.DateTime, nullable=False, default=datetime.datetime.utcnow 14 | ) 15 | updated_at = db.Column( 16 | db.DateTime, nullable=False, default=datetime.datetime.utcnow 17 | ) 18 | 19 | retailer = db.relationship("Retailer", back_populates="images") 20 | 21 | def to_dict(self): 22 | return { 23 | "id": self.id, 24 | "retailer_id": self.retailer_id, 25 | "image_url": self.image_url, 26 | "created_at": self.created_at, 27 | } 28 | -------------------------------------------------------------------------------- /migrations/versions/20210305_151900_update_comments_model.py: -------------------------------------------------------------------------------- 1 | """update comments model 2 | 3 | Revision ID: 32b61a573ad8 4 | Revises: f1ba629b1857 5 | Create Date: 2021-03-05 15:19:00.691588 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '32b61a573ad8' 13 | down_revision = 'f1ba629b1857' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.alter_column('comments', 'comment_id', 21 | existing_type=sa.INTEGER(), 22 | nullable=True) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.alter_column('comments', 'comment_id', 29 | existing_type=sa.INTEGER(), 30 | nullable=False) 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /api/migrations/versions/20210305_151900_update_comments_model.py: -------------------------------------------------------------------------------- 1 | """update comments model 2 | 3 | Revision ID: 32b61a573ad8 4 | Revises: f1ba629b1857 5 | Create Date: 2021-03-05 15:19:00.691588 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '32b61a573ad8' 13 | down_revision = 'f1ba629b1857' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.alter_column('comments', 'comment_id', 21 | existing_type=sa.INTEGER(), 22 | nullable=True) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.alter_column('comments', 'comment_id', 29 | existing_type=sa.INTEGER(), 30 | nullable=False) 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /api/migrations/versions/20210322_211305_create_lat_and_lng_on_meetups.py: -------------------------------------------------------------------------------- 1 | """create lat and lng on meetups 2 | 3 | Revision ID: 3fc0203ff0f0 4 | Revises: e3919cd21c9b 5 | Create Date: 2021-03-22 21:13:05.056978 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '3fc0203ff0f0' 13 | down_revision = 'e3919cd21c9b' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('meetups', sa.Column('lat', sa.Numeric(scale=7), nullable=True)) 21 | op.add_column('meetups', sa.Column('lng', sa.Numeric(scale=7), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('meetups', 'lng') 28 | op.drop_column('meetups', 'lat') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/20210322_211305_create_lat_and_lng_on_meetups.py: -------------------------------------------------------------------------------- 1 | """create lat and lng on meetups 2 | 3 | Revision ID: 3fc0203ff0f0 4 | Revises: e3919cd21c9b 5 | Create Date: 2021-03-22 21:13:05.056978 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '3fc0203ff0f0' 13 | down_revision = 'e3919cd21c9b' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('meetups', sa.Column('lat', sa.Numeric(scale=7), nullable=True)) 21 | op.add_column('meetups', sa.Column('lng', sa.Numeric(scale=7), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('meetups', 'lng') 28 | op.drop_column('meetups', 'lat') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from app.models.comment import Comment # noqa 2 | from app.models.comment_rating import CommentRating # noqa 3 | from app.models.community import Community # noqa 4 | from app.models.db import db # noqa 5 | from app.models.meetup import Meetup # noqa 6 | from app.models.message import Message # noqa 7 | from app.models.post import Post # noqa 8 | from app.models.post_image import PostsImage # noqa 9 | from app.models.post_rating import PostRating # noqa 10 | from app.models.posts_tag import posts_tags # noqa 11 | from app.models.retailer import Retailer # noqa 12 | from app.models.retailer_image import RetailerImage # noqa 13 | from app.models.retailer_rating import RetailerRating # noqa 14 | from app.models.saved_comment import saved_comments # noqa 15 | from app.models.saved_post import saved_posts # noqa 16 | from app.models.tag import Tag # noqa 17 | from app.models.thread import Thread # noqa 18 | from app.models.user import User # noqa 19 | -------------------------------------------------------------------------------- /api/migrations/versions/20210312_133251_add_lat_and_lng_columns_to_retailer.py: -------------------------------------------------------------------------------- 1 | """add lat and lng columns to retailer 2 | 3 | Revision ID: e3919cd21c9b 4 | Revises: 32b61a573ad8 5 | Create Date: 2021-03-12 13:32:51.555136 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'e3919cd21c9b' 13 | down_revision = '32b61a573ad8' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('retailers', sa.Column('lat', sa.Numeric(scale=7), nullable=True)) 21 | op.add_column('retailers', sa.Column('lng', sa.Numeric(scale=7), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('retailers', 'lng') 28 | op.drop_column('retailers', 'lat') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/20210312_133251_add_lat_and_lng_columns_to_retailer.py: -------------------------------------------------------------------------------- 1 | """add lat and lng columns to retailer 2 | 3 | Revision ID: e3919cd21c9b 4 | Revises: 32b61a573ad8 5 | Create Date: 2021-03-12 13:32:51.555136 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'e3919cd21c9b' 13 | down_revision = '32b61a573ad8' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column('retailers', sa.Column('lat', sa.Numeric(scale=7), nullable=True)) 21 | op.add_column('retailers', sa.Column('lng', sa.Numeric(scale=7), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('retailers', 'lng') 28 | op.drop_column('retailers', 'lat') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /app/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from app import create_app 4 | from app.models import User, db 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def app(): 9 | app = create_app(testing=True) 10 | yield app 11 | 12 | 13 | @pytest.fixture(scope="session") 14 | def client(app): 15 | with app.test_client() as client: 16 | with app.app_context(): 17 | db.drop_all() 18 | db.create_all() 19 | yield client 20 | 21 | 22 | @pytest.fixture(scope="session") 23 | def login_test_user(client): 24 | email = "john@test.com" 25 | username = "johnthetester" 26 | password = "password" 27 | User.create(username=username, email=email, password=password) 28 | login_response = client.post( 29 | "/auth/login", 30 | json={ 31 | "credential": email, 32 | "password": password, 33 | }, 34 | ) 35 | json_login_response = login_response.get_json() 36 | return json_login_response["access_token"] 37 | -------------------------------------------------------------------------------- /app/models/community.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from app.models.db import db 4 | 5 | 6 | class Community(db.Model): 7 | __tablename__ = "communities" 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | name = db.Column(db.String(50), nullable=False) 11 | description = db.Column(db.Text, nullable=False) 12 | created_at = db.Column( 13 | db.DateTime, nullable=False, default=datetime.datetime.utcnow 14 | ) 15 | updated_at = db.Column( 16 | db.DateTime, nullable=False, default=datetime.datetime.utcnow 17 | ) 18 | 19 | posts = db.relationship("Post", back_populates="community") 20 | 21 | def to_simple_dict(self): 22 | return { 23 | "id": self.id, 24 | "name": self.name, 25 | } 26 | 27 | def to_dict(self): 28 | return { 29 | "id": self.id, 30 | "name": self.name, 31 | "description": self.description, 32 | "created_at": self.created_at, 33 | } 34 | -------------------------------------------------------------------------------- /app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import ConfigDict 4 | 5 | from app.schemas.comment import MinimalCommentResponse 6 | from app.schemas.config import BaseORMModeModel 7 | from app.schemas.meetup import MinimalMeetupResponse 8 | from app.schemas.post import MinimalPostResponse 9 | from app.schemas.retailer import MinimalRetailerResponse 10 | 11 | 12 | class MinimalOrmObjectResponse(BaseORMModeModel): 13 | id: int 14 | 15 | 16 | class MinimalUserResponse(BaseORMModeModel): 17 | id: int 18 | username: str 19 | email: str 20 | created_at: datetime 21 | 22 | 23 | class FullUserResponse(MinimalUserResponse): 24 | meetups: list[MinimalMeetupResponse] 25 | saved_posts: list[MinimalPostResponse] 26 | saved_comments: list[MinimalCommentResponse] 27 | comments: list[MinimalCommentResponse] 28 | posts: list[MinimalPostResponse] 29 | retailers: list[MinimalRetailerResponse] 30 | 31 | model_config = ConfigDict(from_attributes=True) 32 | -------------------------------------------------------------------------------- /api/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | script_location = migrations 5 | file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 6 | prepend_sys_path = .. 7 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 8 | sqlalchemy.url = 9 | 10 | [post_write_hooks] 11 | 12 | [loggers] 13 | keys = root,sqlalchemy,alembic 14 | 15 | [handlers] 16 | keys = console 17 | 18 | [formatters] 19 | keys = generic 20 | 21 | [logger_root] 22 | level = WARN 23 | handlers = console 24 | qualname = 25 | 26 | [logger_sqlalchemy] 27 | level = WARN 28 | handlers = console 29 | qualname = sqlalchemy.engine 30 | 31 | [logger_alembic] 32 | level = INFO 33 | handlers = console 34 | qualname = alembic 35 | 36 | [handler_console] 37 | class = StreamHandler 38 | args = (sys.stderr,) 39 | level = NOTSET 40 | formatter = generic 41 | 42 | [formatter_generic] 43 | format = %(levelname)-5.5s [%(name)s] %(message)s 44 | datefmt = %H:%M:%S 45 | -------------------------------------------------------------------------------- /react-app/src/components/parts/InputField/InputField.js: -------------------------------------------------------------------------------- 1 | const InputField = ({ name, type, placeholder, value, onChange }) => { 2 | if (type === 'textarea') { 3 | return ( 4 |
5 |