diff --git a/.flaskenv b/.flaskenv index cef6a2b..5b36094 100644 --- a/.flaskenv +++ b/.flaskenv @@ -1,2 +1,2 @@ -FLASK_APP=microblog.py +FLASK_APP=carekeepr.py FLASK_DEBUG=1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 17ffae7..2f63e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ venv /**/*.pyc logs app.db -static/css \ No newline at end of file +static/css +.eggs/ diff --git a/Procfile b/Procfile index 5d0b597..f4620a2 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: flask db upgrade; flask translate compile; gunicorn microblog:app \ No newline at end of file +web: flask db upgrade; flask translate compile; gunicorn carekeepr:app \ No newline at end of file diff --git a/app.db b/app.db index 0bf2847..0be3891 100644 Binary files a/app.db and b/app.db differ diff --git a/app/__init__.py b/app/__init__.py index aee5e0d..0eceba5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,6 +8,8 @@ from flask_mail import Mail from flask_bootstrap import Bootstrap from config import Config +from flask_uploads import UploadSet, configure_uploads, IMAGES +from flask_scss import Scss db = SQLAlchemy() migrate = Migrate() @@ -16,16 +18,18 @@ login.login_message = 'Please log in to access this page.' mail = Mail() bootstrap = Bootstrap() +photos = UploadSet('photos', IMAGES) def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) - + Scss(app,static_dir='app/static', asset_dir='app/assets') db.init_app(app) migrate.init_app(app, db) login.init_app(app) mail.init_app(app) bootstrap.init_app(app) + configure_uploads(app, photos) from app.errors import bp as errors_bp app.register_blueprint(errors_bp) @@ -47,7 +51,7 @@ def create_app(config_class=Config): mail_handler = SMTPHandler( mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), fromaddr='no-reply@' + app.config['MAIL_SERVER'], - toaddrs=app.config['ADMINS'], subject='Microblog Failure', + toaddrs=app.config['ADMINS'], subject='Carekeepr Failure', credentials=auth, secure=secure) mail_handler.setLevel(logging.ERROR) app.logger.addHandler(mail_handler) @@ -60,7 +64,7 @@ def create_app(config_class=Config): if not os.path.exists('logs'): os.mkdir('logs') - file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240, + file_handler = RotatingFileHandler('logs/Carekeepr.log', maxBytes=10240, backupCount=10) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) @@ -68,7 +72,7 @@ def create_app(config_class=Config): app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) - app.logger.info('Microblog startup') + app.logger.info('Carekeepr startup') return app diff --git a/app/assets/scss/_site.scss b/app/assets/scss/_site.scss new file mode 100644 index 0000000..b70d9b2 --- /dev/null +++ b/app/assets/scss/_site.scss @@ -0,0 +1,128 @@ +$primary-color:#19234f; +$secondary-color: #90be3e; +$primary-background: #eeeff1; +$button-hover: #7da437; +body { + background-color: $primary-background; + color: $primary-color; +} + +.logo { + max-height: 25px; +} + +.title { + text-align: center; +} + +a { + text-decoration: none !important; +} + +.navbar-default .navbar-nav { + margin: 10px; + li { + a { + width: 75px; + padding: 5px; + text-align: center; + &:hover { + color: #fff; + background-color: $button-hover; + border-radius: 40px; + } + } + } +} + +.box { + border: solid 1px; + text-align: center; + min-height: 300px; + margin: 10px; + transition: all 0.5s; + div.care { + background-color: $secondary-color; + color: $primary-background; + padding: 10px; + margin-top: 50%; + font-size: 20px; + } + &:hover { + opacity: 0.9; + border: solid 4px; + } +} + +.profile-sm { + width: 70px; +} + +.profile-lg { + width: 150px; +} + +.caregiver { + background-image: url('/static/img/caregiver01.jpg'); + background-size: cover; +} + +.careseeker { + background-image: url('/static/img/caregiver.jpg'); + background-size: cover; +} + +.profile { + div { + padding: 3px; + } + h3 { + background-color: $primary-color; + width: 100%; + padding: 10px; + text-align: center; + color: #fff; + } + h4 { + font-weight: bold; + color: $primary-color; + font-size: 1.5em; + grid-column: 1 / 2; + } + .form-group { + display: grid; + grid-template-columns: 200px 1fr; + border: solid 1px $secondary-color; + } + .description { + grid-column: 2 / 3; + } + textarea { + width: 100%; + height: 100px; + } + .custom-file-upload { + border: 1px solid #ccc; + display: inline-block; + padding: 6px 12px; + cursor: pointer; + background-color: $button-hover; + color: #fff; + &:hover{ + background-color: $primary-color; + } + } + input[type=file] { + display: none; + } + #submit { + float: right; + width: 30%; + background-color: $primary-color; + color: #fff; + &:hover { + background-color: $secondary-color; + } + margin-bottom: 100px; + } +} \ No newline at end of file diff --git a/app/assets/scss/main.scss b/app/assets/scss/main.scss new file mode 100644 index 0000000..ac965d6 --- /dev/null +++ b/app/assets/scss/main.scss @@ -0,0 +1 @@ +@import 'site'; \ No newline at end of file diff --git a/app/auth/email.py b/app/auth/email.py index 52cb1ee..3ee2b47 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -4,7 +4,7 @@ def send_password_reset_email(user): token = user.get_reset_password_token() - send_email('[Microblog] Reset Your Password', + send_email('[Carekeepr] Reset Your Password', sender=current_app.config['ADMINS'][0], recipients=[user.email], text_body=render_template('email/reset_password.txt', diff --git a/app/auth/forms.py b/app/auth/forms.py index 01f500a..596d1e7 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -14,6 +14,8 @@ class LoginForm(FlaskForm): class RegistrationForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) email = StringField('Email', validators=[DataRequired(), Email()]) + firstName = StringField('FirstName', validators=[DataRequired()]) + lastName = StringField('LastName', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) password2 = PasswordField( 'Repeat Password', validators=[DataRequired(), diff --git a/app/auth/routes.py b/app/auth/routes.py index c4fa1e8..571ce7e 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -12,7 +12,7 @@ @bp.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: - return redirect(url_for('main.index')) + return redirect(current_user.username) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() @@ -22,7 +22,7 @@ def login(): login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': - next_page = url_for('main.index') + next_page = user.username return redirect(next_page) return render_template('auth/login.html', title='Sign In', form=form) @@ -39,7 +39,7 @@ def register(): return redirect(url_for('main.index')) form = RegistrationForm() if form.validate_on_submit(): - user = User(username=form.username.data, email=form.email.data) + user = User(username=form.username.data, email=form.email.data, firstName = form.firstName.data, lastName = form.lastName.data) user.set_password(form.password.data) db.session.add(user) db.session.commit() diff --git a/app/forms.py b/app/forms.py deleted file mode 100644 index e4f9c3b..0000000 --- a/app/forms.py +++ /dev/null @@ -1,58 +0,0 @@ -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField -from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length -from app.models import User - -class PostForm(FlaskForm): - post = TextAreaField('Say something', validators=[ - DataRequired(), Length(min=1, max=140)]) - submit = SubmitField('Submit') - -class LoginForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired()]) - remember_me = BooleanField('Remember Me') - submit = SubmitField('Sign In') - -class RegistrationForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - email = StringField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - password2 = PasswordField( - 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) - submit = SubmitField('Register') - - def validate_username(self, username): - user = User.query.filter_by(username=username.data).first() - if user is not None: - raise ValidationError('Please use a different username.') - - def validate_email(self, email): - user = User.query.filter_by(email=email.data).first() - if user is not None: - raise ValidationError('Please use a different email address.') - -class EditProfileForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) - submit = SubmitField('Submit') - - def __init__(self, original_username, *args, **kwargs): - super(EditProfileForm, self).__init__(*args, **kwargs) - self.original_username = original_username - - def validate_username(self, username): - if username.data != self.original_username: - user = User.query.filter_by(username=self.username.data).first() - if user is not None: - raise ValidationError('Please use a different username.') - -class ResetPasswordRequestForm(FlaskForm): - email = StringField('Email', validators=[DataRequired(),Email()]) - submit = SubmitField('Request Password Reset') - -class ResetPasswordForm(FlaskForm): - password = PasswordField('Password', validators=[DataRequired()]) - password2 = PasswordField( - 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) - submit = SubmitField('Request Password Reset') \ No newline at end of file diff --git a/app/main/forms.py b/app/main/forms.py index 2992241..502072e 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1,14 +1,21 @@ from flask import request from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, TextAreaField +from wtforms import StringField, SubmitField, TextAreaField, HiddenField, SelectField from wtforms.validators import ValidationError, DataRequired, Length from app.models import User class EditProfileForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) + pic = HiddenField('pic') + username = StringField('Custom Domain', validators=[DataRequired()]) + email = StringField('Email', validators=[DataRequired()]) + firstName = StringField('FirstName', validators=[DataRequired()]) + lastName = StringField('LastName', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) + availability = TextAreaField('Availability', validators=[Length(min=0, max=140)]) + location = StringField('Location', validators=[Length(min=0, max=140)]) + skills = TextAreaField('Skills', validators=[Length(min=0, max=140)]) submit = SubmitField('Submit') def __init__(self, original_username, *args, **kwargs): @@ -21,9 +28,21 @@ def validate_username(self, username): if user is not None: raise ValidationError('Please use a different username.') +class CareRequestForm(FlaskForm): + careType = SelectField( + 'What type of care do you need?', + choices=[('elderly', 'Elderly Care'), ('child', 'Child Care'), ('pet', 'Pet Care')] + ) + location = StringField('Location', validators=[DataRequired()]) + careFrequency = SelectField( + 'When do you need care?', + choices=[('rare', 'Occasional back-up care'), ('ft', 'Full-Time'), ('pt', 'Part Time')] + ) + needs = TextAreaField('Describe your needs', validators=[Length(min=0, max=200)]) + submit = SubmitField('Submit') class PostForm(FlaskForm): - post = TextAreaField('Say something', validators=[DataRequired()]) + post = TextAreaField('Send Message', validators=[DataRequired()]) submit = SubmitField('Submit') @@ -36,3 +55,9 @@ def __init__(self, *args, **kwargs): if 'csrf_enabled' not in kwargs: kwargs['csrf_enabled'] = False super(SearchForm, self).__init__(*args, **kwargs) + +class MessageForm(FlaskForm): + message = TextAreaField('Message', validators=[ + DataRequired(), Length(min=1, max=140)]) + submit = SubmitField('Submit') + diff --git a/app/main/routes.py b/app/main/routes.py index 116afcc..1df6a7b 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,12 +1,14 @@ +import os, boto3, json from datetime import datetime from flask import render_template, flash, redirect, url_for, request, g, \ jsonify, current_app from flask_login import current_user, login_required -from app import db -from app.main.forms import EditProfileForm, PostForm, SearchForm -from app.models import User, Post +from app import db, photos +from app.main.forms import EditProfileForm, PostForm, SearchForm, CareRequestForm, MessageForm +from app.models import User, Post, CareRequest, Message from app.main import bp - +from werkzeug.utils import secure_filename +from config import basedir @bp.before_app_request def before_request(): @@ -17,14 +19,19 @@ def before_request(): @bp.route('/') def index(): + return redirect(url_for('auth.login')) + #return render_template('index.html', title='Explore') + +@bp.route('/list') +def lists(): page = request.args.get('page', 1, type=int) users = User.query.paginate( page, current_app.config['USERS_PER_PAGE'], False) - next_url = url_for('main.index', page=users.next_num) \ + next_url = url_for('main.lists', page=users.next_num) \ if users.has_next else None - prev_url = url_for('main.index', page=users.prev_num) \ + prev_url = url_for('main.lists', page=users.prev_num) \ if users.has_prev else None - return render_template('index.html', title='Explore', + return render_template('list.html', title='Explore', users=users.items, next_url=next_url, prev_url=prev_url) @@ -64,29 +71,29 @@ def explore(): posts=posts.items, next_url=next_url, prev_url=prev_url) - -@bp.route('/user/', methods=['GET', 'POST']) -@login_required -def user(username): - form = PostForm() +@bp.route('/request_care', methods=['GET', 'POST']) +def request_care(): + form = CareRequestForm() if form.validate_on_submit(): - post = Post(body=form.post.data, author=current_user) - db.session.add(post) - db.session.commit() - flash('Your post is now live!') - return redirect(url_for('main.explore')) - - user = User.query.filter_by(username=username).first_or_404() - page = request.args.get('page', 1, type=int) - posts = user.posts.order_by(Post.timestamp.desc()).paginate( - page, current_app.config['POSTS_PER_PAGE'], False) - next_url = url_for('main.user', username=user.username, - page=posts.next_num) if posts.has_next else None - prev_url = url_for('main.user', username=user.username, - page=posts.prev_num) if posts.has_prev else None - return render_template('user.html', user=user, form=form, posts=posts.items, - next_url=next_url, prev_url=prev_url) - + care_request = CareRequest() + care_request.caretype = form.careType.data + care_request.location = form.location.data + care_request.careFrequency = form.careFrequency.data + care_request.needs = form.needs.data + try: + db.session.commit() + flash('Your request has been saved.') + return redirect(url_for('main.lists')) + except: + return redirect(url_for('main.lists')) + elif request.method == 'POST': + for field, errors in form.errors.items(): + for error in errors: + flash(u"Error in the %s field - %s" % ( + getattr(form, field).label.text, + error + )) + return render_template('request_care.html', title='Care Request',form=form) @bp.route('/edit_profile', methods=['GET', 'POST']) @login_required @@ -94,16 +101,77 @@ def edit_profile(): form = EditProfileForm(current_user.username) if form.validate_on_submit(): current_user.username = form.username.data + current_user.email = form.email.data + current_user.firstName = form.firstName.data + current_user.lastName = form.lastName.data current_user.about_me = form.about_me.data - db.session.commit() - flash('Your changes have been saved.') + current_user.availability = form.availability.data + current_user.location = form.location.data + current_user.skills = form.skills.data + current_user.pic = form.pic.data + try: + db.session.commit() + flash('Your changes have been saved.') + except: + flash('Your username is taken.') return redirect(url_for('main.edit_profile')) elif request.method == 'GET': form.username.data = current_user.username + form.email.data = current_user.email + form.firstName.data = current_user.firstName + form.lastName.data = current_user.lastName form.about_me.data = current_user.about_me - return render_template('edit_profile.html', title='Edit Profile', + form.availability.data = current_user.availability + form.location.data = current_user.location + form.skills.data = current_user.skills + else: + for field, errors in form.errors.items(): + for error in errors: + flash(u"Error in the %s field - %s" % ( + getattr(form, field).label.text, + error + )) + return render_template('edit_profile.html', title='Edit Profile',photo=current_user.pic, form=form) +@bp.route('/upload', methods=['GET', 'POST']) +def upload(): + if request.method == 'POST' and 'photo' in request.files: + image = request.files['photo'] #myfile is name of input tag + filename = secure_filename(image.filename) + fullfile = os.path.join(basedir,'app\\static\\img\\' + current_user.username, filename) + if not os.path.exists(os.path.dirname(fullfile)): + os.makedirs(os.path.dirname(fullfile)) + image.save(fullfile) + current_user.pic = '/static/img/' + current_user.username + '/' + filename + db.session.commit() + flash('Image updated.') + return redirect(url_for('main.edit_profile')) + +@bp.route('/sign_s3', methods=['GET','POST']) +def sign_s3(): + S3_BUCKET = 'care-dev-01' + + file_name = request.args.get('file_name') + file_type = request.args.get('file_type') + + s3 = boto3.client('s3') + + presigned_post = s3.generate_presigned_post( + Bucket = S3_BUCKET, + Key = file_name, + Fields = {"acl": "public-read", "Content-Type": file_type}, + Conditions = [ + {"acl": "public-read"}, + {"Content-Type": file_type} + ], + ExpiresIn = 3600 + ) + + return json.dumps({ + 'data': presigned_post, + 'url': 'https://%s.s3.amazonaws.com/%s' % (S3_BUCKET, file_name) + }) @bp.route('/follow/') @login_required @@ -150,3 +218,60 @@ def search(): if page > 1 else None return render_template('search.html', title='Search', posts=posts, next_url=next_url, prev_url=prev_url) + +@bp.route('/send_message/', methods=['GET', 'POST']) +@login_required +def send_message(recipient): + user = User.query.filter_by(username=recipient).first_or_404() + form = MessageForm() + if form.validate_on_submit(): + msg = Message(author=current_user, recipient=user, + body=form.message.data) + db.session.add(msg) + user.add_notification('unread_message_count', user.new_messages()) + db.session.commit() + flash('Your message has been sent.') + return redirect(url_for('main.user', username=recipient)) + return render_template('send_message.html', title='Send Message', + form=form, recipient=recipient) + + +@bp.route('/messages') +@login_required +def messages(): + current_user.last_message_read_time = datetime.utcnow() + current_user.add_notification('unread_message_count', 0) + db.session.commit() + page = request.args.get('page', 1, type=int) + messages = current_user.messages_received.order_by( + Message.timestamp.desc()).paginate( + page, current_app.config['POSTS_PER_PAGE'], False) + next_url = url_for('messages', page=messages.next_num) \ + if messages.has_next else None + prev_url = url_for('messages', page=messages.prev_num) \ + if messages.has_prev else None + return render_template('messages.html', messages=messages.items, + next_url=next_url, prev_url=prev_url) + + +@bp.route('/', methods=['GET', 'POST']) +@login_required +def user(username): + form = PostForm() + if form.validate_on_submit(): + post = Post(body=form.post.data, author=current_user) + db.session.add(post) + db.session.commit() + flash('Your message was sent!') + return redirect(url_for('main.explore')) + + user = User.query.filter_by(username=username).first_or_404() + page = request.args.get('page', 1, type=int) + posts = user.posts.order_by(Post.timestamp.desc()).paginate( + page, current_app.config['POSTS_PER_PAGE'], False) + next_url = url_for('main.user', username=user.username, + page=posts.next_num) if posts.has_next else None + prev_url = url_for('main.user', username=user.username, + page=posts.prev_num) if posts.has_prev else None + return render_template('user.html', user=user, form=form, posts=posts.items, + next_url=next_url, prev_url=prev_url) diff --git a/app/models.py b/app/models.py index f7de1fe..24ebdcb 100644 --- a/app/models.py +++ b/app/models.py @@ -5,6 +5,7 @@ from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash import jwt +import json from app import db, login followers = db.Table( @@ -17,10 +18,24 @@ class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) + firstName = db.Column(db.String(20)) + lastName = db.Column(db.String(20)) password_hash = db.Column(db.String(128)) posts = db.relationship('Post', backref='author', lazy='dynamic') about_me = db.Column(db.String(140)) + availability = db.Column(db.String(140)) + location = db.Column(db.String(140)) + skills = db.Column(db.String(140)) + pic = db.Column(db.String(100)) last_seen = db.Column(db.DateTime, default=datetime.utcnow) + messages_sent = db.relationship('Message', foreign_keys='Message.sender_id', + backref='author', lazy='dynamic') + messages_received = db.relationship('Message', foreign_keys='Message.recipient_id', + backref='recipient', lazy='dynamic') + last_message_read_time = db.Column(db.DateTime) + notifications = db.relationship('Notification', backref='user', + lazy='dynamic') + followed = db.relationship( 'User', secondary=followers, @@ -71,6 +86,17 @@ def get_reset_password_token(self, expires_in=600): current_app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8') + def new_messages(self): + last_read_time = self.last_message_read_time or datetime(1900,1,1) + return Message.query.filter_by(recipient=self).filter( + Message.timestamp > last_read_time).count() + + def add_notification(self, name, data): + self.notifications.filter_by(name=name).delete() + n = Notification(name=name, payload_json=json.dumps(data), user=self) + db.session.add(n) + return n + @staticmethod def verify_reset_password_token(token): try: @@ -83,7 +109,18 @@ def verify_reset_password_token(token): @login.user_loader def load_user(id): return User.query.get(int(id)) - + +class CareRequest(db.Model): + id = db.Column(db.Integer, primary_key=True) + careType = db.Column(db.String(20)) + location = db.Column(db.String(100)) + careFrequency = db.Column(db.String(20)) + needs = db.Column(db.String(200)) + timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) + + def __repr__(self): + return ''.format(self.body) + class Post(db.Model): id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String(140)) @@ -93,5 +130,22 @@ class Post(db.Model): def __repr__(self): return ''.format(self.body) +class Message(db.Model): + id = db.Column(db.Integer, primary_key=True) + sender_id = db.Column(db.Integer, db.ForeignKey('user.id')) + recipient_id = db.Column(db.Integer, db.ForeignKey('user.id')) + body = db.Column(db.String(140)) + timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) + + def __repr__(self): + return ''.format(self.body) +class Notification(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(128), index=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + timestamp = db.Column(db.Float, index=True, default=time) + payload_json = db.Column(db.Text) + def get_data(self): + return json.loads(str(self.payload_json)) \ No newline at end of file diff --git a/app/static/css/main.css b/app/static/css/main.css new file mode 100644 index 0000000..df127f1 --- /dev/null +++ b/app/static/css/main.css @@ -0,0 +1,95 @@ +body { + background-color: #eeeff1; + color: #19234f; } + + .logo { + max-height: 25px; } + + .title { + text-align: center; } + + a { + text-decoration: none !important; } + + .navbar-default .navbar-nav { + margin: 10px; } + .navbar-default .navbar-nav li a { + width: 75px; + padding: 5px; + text-align: center; } + .navbar-default .navbar-nav li a:hover { + color: #fff; + background-color: #7da437; + border-radius: 40px; } + +.box { + border: solid 1px; + text-align: center; + min-height: 300px; + margin: 10px; + transition: all 0.5s; } + .box div.care { + background-color: #90be3e; + color: #eeeff1; + padding: 10px; + margin-top: 50%; + font-size: 20px; } + .box:hover { + opacity: 0.9; + border: solid 4px; } + +.profile-sm { + width: 70px; } + + .profile-lg { + width: 150px; } + + .caregiver { + background-image: url('/static/img/caregiver01.jpg'); + background-size: cover; } + + .careseeker { + background-image: url('/static/img/caregiver.jpg'); + background-size: cover; } + + .profile div { + padding: 3px; } + .profile h3 { + background-color: #19234f; + width: 100%; + padding: 10px; + text-align: center; + color: #fff; } + .profile h4 { + font-weight: bold; + color: #19234f; + font-size: 1.5em; + grid-column: 1 / 2; } + .profile .form-group { + display: grid; + grid-template-columns: 200px 1fr; + border: solid 1px #90be3e; } + .profile .description { + grid-column: 2 / 3; } + .profile textarea { + width: 100%; + height: 100px; } + .profile .custom-file-upload { + border: 1px solid #ccc; + display: inline-block; + padding: 6px 12px; + cursor: pointer; + background-color: #7da437; + color: #fff; } + .profile .custom-file-upload:hover { + background-color: #19234f; } + .profile input[type=file] { + display: none; } + .profile #submit { + float: right; + width: 30%; + background-color: #19234f; + color: #fff; + margin-bottom: 100px; } + .profile #submit:hover { + background-color: #90be3e; } diff --git a/app/static/css/main.scss.css b/app/static/css/main.scss.css deleted file mode 100644 index 7863191..0000000 --- a/app/static/css/main.scss.css +++ /dev/null @@ -1,6 +0,0 @@ -body { - background-color: #eeeff1; - color: #19234f; } - -.logo { - max-height: 25px; } diff --git a/app/static/img/caregiver.jpg b/app/static/img/caregiver.jpg new file mode 100644 index 0000000..ea519c5 Binary files /dev/null and b/app/static/img/caregiver.jpg differ diff --git a/app/static/img/caregiver01.jpg b/app/static/img/caregiver01.jpg new file mode 100644 index 0000000..c0ab4d4 Binary files /dev/null and b/app/static/img/caregiver01.jpg differ diff --git a/app/static/img/default-image.jpg b/app/static/img/default-image.jpg new file mode 100644 index 0000000..7f990bd Binary files /dev/null and b/app/static/img/default-image.jpg differ diff --git a/app/static/img/logo.png b/app/static/img/logo.png new file mode 100644 index 0000000..319ef87 Binary files /dev/null and b/app/static/img/logo.png differ diff --git a/app/static/scripts/main.js b/app/static/scripts/main.js new file mode 100644 index 0000000..19d65ef --- /dev/null +++ b/app/static/scripts/main.js @@ -0,0 +1,61 @@ +$(document).ready(function($) { + $(".clickable-row").click(function() { + window.location = $(this).data("href"); + }); + + $('#photo').on('change',function(){ + var files = $("#photo")[0].files; + var file = files[0]; + if (!file) { + return alert("No file selected."); + } + if (file.type != "image/jpeg" && file.type != "image/png"){ + return alert("Please upload a jpg or png file"); + } + if (file.size > 2000000){ + return alert("File must be under 2mb"); + } + getSignedRequest(file); + }); +}); + +function uploadFile(file, s3Data, url){ + var xhr = new XMLHttpRequest(); + xhr.open("POST", s3Data.url); + + var postData = new FormData(); + for(key in s3Data.fields){ + postData.append(key, s3Data.fields[key]); + } + postData.append('file', file); + + xhr.onreadystatechange = function() { + if(xhr.readyState === 4){ + if(xhr.status === 200 || xhr.status === 204){ + document.getElementById("preview").src = url; + document.getElementById("pic").value = url; + } + else{ + alert("Could not upload file."); + } + } + }; + xhr.send(postData); + } + +function getSignedRequest(file){ + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/sign_s3?file_name="+file.name+"&file_type="+file.type); + xhr.onreadystatechange = function(){ + if(xhr.readyState === 4){ + if(xhr.status === 200){ + var response = JSON.parse(xhr.responseText); + uploadFile(file, response.data, response.url); + } + else{ + alert("Could not get signed URL."); + } + } + }; + xhr.send(); + } \ No newline at end of file diff --git a/app/static/scss/main.scss b/app/static/scss/main.scss deleted file mode 100644 index 4417408..0000000 --- a/app/static/scss/main.scss +++ /dev/null @@ -1,12 +0,0 @@ -$primary-color:#19234f; -$secondary-color: #90be3e; -$primary-background: #eeeff1; - -body{ - background-color: $primary-background; - color: $primary-color; -} - -.logo{ - max-height: 25px; -} \ No newline at end of file diff --git a/app/templates/_post.html b/app/templates/_post.html index e372b1b..fe96edc 100644 --- a/app/templates/_post.html +++ b/app/templates/_post.html @@ -2,7 +2,11 @@ - + {% if post.author.pic != None %} + + {% else %} + + {% endif %} diff --git a/app/templates/_users.html b/app/templates/_users.html index 6ba4d09..21e97a0 100644 --- a/app/templates/_users.html +++ b/app/templates/_users.html @@ -1,12 +1,16 @@ diff --git a/app/templates/edit_profile.html b/app/templates/edit_profile.html index 94f9202..c9237ca 100644 --- a/app/templates/edit_profile.html +++ b/app/templates/edit_profile.html @@ -1,11 +1,85 @@ {% extends "layout.html" %} {% import 'bootstrap/wtf.html' as wtf %} - {% block app_content %} -

Edit Profile

-
-
- {{ wtf.quick_form(form) }} +
+
+

Let's edit your profile

+

A little about you

+

Photo

+

+ Here you can upload a photo so people can recognize you. It improves + requests a lot. Don't you like to see who you are working with? +

+ {% if photo %} + + {% else %} + + {% endif %} +
+ +
+
+ {{ form.csrf_token }} + {{ form.pic }} +
+

Your link for others

+
https://caregive-dev.herokuapp.com/{{ form.username(class="form_control") }}
+

+ Choose a name for your website or business. The name you choose will be your user name as well. It + will show up as www.CareKeepr.com/your name. This will be your link to share your new site with + friends, + family, clients, social organizations or social media. Please choose this name wisely as this will + be a public URL. If privacy is an issue, do not use full name. +

+
+
+

Your email address

+
{{ form.email(class="form_control") }}
+

+ This is where we will send private messages and notifications from people looking for you. + Don't worry this address will not be seen by anyone publicly. It is how we can reach out to you + when your new clients are looking for you. It will be up to you to share it with them. +

+
+
+

Your First and Last Name

+
{{ form.firstName(class="form_control") }} {{ form.lastName(class="form_control") }}
+

+ This is your first and last name. We will only display your first name to the public. +

+
+
+

{{ form.about_me.label }}

+
{{ form.about_me(class="form_control") }}
+

+ This is where you will tell people about who you are and why they should hire you. +

+
+

A little about you location and skills

+
+

{{ form.availability.label }}

+
{{ form.availability(class="form_control") }}
+

+ This is your availability for care. Example: M-F 10-4pm, No Weekends, etc.. +

+
+
+

{{ form.location.label }}

+
{{ form.location(class="form_control") }}
+

+ This is your general location so we can match you with people in your area. +

+
+
+

{{ form.skills.label }}

+
{{ form.skills(class="form_control") }}
+

+ This is a list of your skills and certifications. +

+
+
{{ form.submit(class="btn btn-default",id='submit',value="Save") }}
+
+
{% endblock %} \ No newline at end of file diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html index 4c9e490..a0bd8e9 100644 --- a/app/templates/email/reset_password.html +++ b/app/templates/email/reset_password.html @@ -9,4 +9,4 @@

{{ url_for('auth.reset_password', token=token, _external=True) }}

If you have not requested a password reset simply ignore this message.

Sincerely,

-

The Microblog Team

\ No newline at end of file +

The Carekeepr Team

\ No newline at end of file diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt index cf5c679..ac79708 100644 --- a/app/templates/email/reset_password.txt +++ b/app/templates/email/reset_password.txt @@ -8,4 +8,4 @@ If you have not requested a password reset simply ignore this message. Sincerely, -The Microblog Team \ No newline at end of file +The Carekeepr Team \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 4ffed3f..f03c5e8 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,30 +1,21 @@ -{% extends "layout.html" %} -{% import 'bootstrap/wtf.html' as wtf %} - +{% extends "layout.html" %} +{% import 'bootstrap/wtf.html' as wtf %} {% block app_content %} -

Welcome to CareKeepr

- {% for user in users %} - {% include '_users.html' %} - {% endfor %} - - +

Welcome to CareKeepr

+ {% endblock %} \ No newline at end of file diff --git a/app/templates/layout.html b/app/templates/layout.html index 377c4cf..c0632e1 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -4,11 +4,12 @@ {% endblock %} {% block head %} {{ super() }} - + {% endblock %} {% block navbar %}
- + {% if user.pic != None %} + + {% else %} + + {% endif %} - Name: {{ user.username }} -
- About Me: {{ user.about_me }} +
Name: {{ user.username }}
+
About Me: {{ user.about_me }}
+
Location: {{ user.location }}
- +
+ {% if user.pic != None %} + + {% else %} + + {% endif %} +

User: {{ user.username }}

+ {% if user.firstName and user.lastName %}

{{ user.firstName }} {{ user.lastName }}

{% endif %} {% if user.about_me %}

{{ user.about_me }}

{% endif %} - {% if user.last_seen %}

Last seen on: {{ user.last_seen }}

{% endif %} -

{{ user.followers.count() }} followers, {{ user.followed.count() }} following.

- {% if user == current_user %} -

Edit your profile

- {% elif not current_user.is_following(user) %} -

Follow

- {% else %} -

Unfollow

- {% endif %} + {% if user.location %}

{{ user.location }}

{% endif %} + {% if user.availability %}

{{ user.availability }}

{% endif %} + {% if user.skills %}

{{ user.skills }}

{% endif %} + {% if user.last_seen %}

Last seen on: {{ user.last_seen }}

{% endif %} +

{{ user.followers.count() }} followers, {{ user.followed.count() }} following.

+ {% if user == current_user %} +

Edit your profile

+ {% elif not current_user.is_following(user) %} +

Follow

+ {% else %} +

Unfollow

+ {% endif %} + {% if user != current_user %} +

Send private message

+ {% endif %}
@@ -27,21 +40,24 @@

User: {{ user.username }}

{% endif %} - {% for post in posts %} - {% include '_post.html' %} - {% endfor %} - + {% if user == current_user %} +

Your Messages

+ {% for post in posts %} + {% include '_post.html' %} + {% endfor %} + + {% endif %} {% endblock %} \ No newline at end of file diff --git a/microblog.py b/carekeepr.py similarity index 100% rename from microblog.py rename to carekeepr.py diff --git a/config.py b/config.py index b91899b..48aa9c6 100644 --- a/config.py +++ b/config.py @@ -16,7 +16,8 @@ class Config(object): MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') ADMINS = ['your-email@example.com'] - + UPLOADED_PHOTOS_DEST = 'app/static/img' + POSTS_PER_PAGE = 3 USERS_PER_PAGE = 3 diff --git a/migrations/versions/108b193343d3_added_first_and_last_name.py b/migrations/versions/108b193343d3_added_first_and_last_name.py new file mode 100644 index 0000000..af4179d --- /dev/null +++ b/migrations/versions/108b193343d3_added_first_and_last_name.py @@ -0,0 +1,30 @@ +"""Added first and last name + +Revision ID: 108b193343d3 +Revises: 8d4ee1677fb2 +Create Date: 2018-09-02 11:45:12.065477 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '108b193343d3' +down_revision = '8d4ee1677fb2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('firstName', sa.String(length=20), nullable=True)) + op.add_column('user', sa.Column('lastName', sa.String(length=20), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'lastName') + op.drop_column('user', 'firstName') + # ### end Alembic commands ### diff --git a/migrations/versions/4b9e41630dfb_added_care_request.py b/migrations/versions/4b9e41630dfb_added_care_request.py new file mode 100644 index 0000000..22bab7d --- /dev/null +++ b/migrations/versions/4b9e41630dfb_added_care_request.py @@ -0,0 +1,49 @@ +"""Added care request + +Revision ID: 4b9e41630dfb +Revises: 108b193343d3 +Create Date: 2018-09-22 06:58:45.821582 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b9e41630dfb' +down_revision = '108b193343d3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('care_request', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('careType', sa.String(length=20), nullable=True), + sa.Column('location', sa.String(length=100), nullable=True), + sa.Column('careFrequency', sa.String(length=20), nullable=True), + sa.Column('needs', sa.String(length=200), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_care_request_timestamp'), 'care_request', ['timestamp'], unique=False) + op.create_table('message', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('body', sa.String(length=140), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_message_timestamp'), 'message', ['timestamp'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_message_timestamp'), table_name='message') + op.drop_table('message') + op.drop_index(op.f('ix_care_request_timestamp'), table_name='care_request') + op.drop_table('care_request') + # ### end Alembic commands ### diff --git a/migrations/versions/75b2b926d295_added_user_fields.py b/migrations/versions/75b2b926d295_added_user_fields.py new file mode 100644 index 0000000..ec024e3 --- /dev/null +++ b/migrations/versions/75b2b926d295_added_user_fields.py @@ -0,0 +1,32 @@ +"""added user fields + +Revision ID: 75b2b926d295 +Revises: 0fa1361daf0c +Create Date: 2018-09-01 17:44:00.008705 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '75b2b926d295' +down_revision = '0fa1361daf0c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('availability', sa.String(length=140), nullable=True)) + op.add_column('user', sa.Column('location', sa.String(length=140), nullable=True)) + op.add_column('user', sa.Column('skills', sa.String(length=140), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'skills') + op.drop_column('user', 'location') + op.drop_column('user', 'availability') + # ### end Alembic commands ### diff --git a/migrations/versions/8d4ee1677fb2_photos.py b/migrations/versions/8d4ee1677fb2_photos.py new file mode 100644 index 0000000..eeac125 --- /dev/null +++ b/migrations/versions/8d4ee1677fb2_photos.py @@ -0,0 +1,28 @@ +"""photos + +Revision ID: 8d4ee1677fb2 +Revises: 75b2b926d295 +Create Date: 2018-09-01 20:59:38.072793 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8d4ee1677fb2' +down_revision = '75b2b926d295' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('pic', sa.String(length=100), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'pic') + # ### end Alembic commands ### diff --git a/migrations/versions/b7bb9c926f36_added_notifications_and_messages.py b/migrations/versions/b7bb9c926f36_added_notifications_and_messages.py new file mode 100644 index 0000000..4aa2557 --- /dev/null +++ b/migrations/versions/b7bb9c926f36_added_notifications_and_messages.py @@ -0,0 +1,55 @@ +"""added notifications and messages + +Revision ID: b7bb9c926f36 +Revises: 4b9e41630dfb +Create Date: 2018-10-27 06:47:54.659866 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b7bb9c926f36' +down_revision = '4b9e41630dfb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('last_message_read_time', sa.DateTime(), nullable=True)) + op.drop_table('notification') + op.create_table('notification', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=128), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('timestamp', sa.Float(), nullable=True), + sa.Column('payload_json', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_notification_name'), 'notification', ['name'], unique=False) + op.create_index(op.f('ix_notification_timestamp'), 'notification', ['timestamp'], unique=False) + op.add_column('message', sa.Column('recipient_id', sa.Integer(), nullable=True)) + op.add_column('message', sa.Column('sender_id', sa.Integer(), nullable=True)) + op.drop_constraint(None, 'message', type_='foreignkey') + op.create_foreign_key(None, 'message', 'user', ['sender_id'], ['id']) + op.create_foreign_key(None, 'message', 'user', ['recipient_id'], ['id']) + op.drop_column('message', 'user_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'last_message_read_time') + op.add_column('message', sa.Column('user_id', sa.INTEGER(), nullable=True)) + op.drop_constraint(None, 'message', type_='foreignkey') + op.drop_constraint(None, 'message', type_='foreignkey') + op.create_foreign_key(None, 'message', 'user', ['user_id'], ['id']) + op.drop_column('message', 'sender_id') + op.drop_column('message', 'recipient_id') + op.drop_index(op.f('ix_notification_timestamp'), table_name='notification') + op.drop_index(op.f('ix_notification_name'), table_name='notification') + op.drop_table('notification') + # ### end Alembic commands ### diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c56a9e4 --- /dev/null +++ b/readme.md @@ -0,0 +1,21 @@ +# My Flask Project +## build sass +``` +pip install Flask-Scss +``` +add it to __init__.py +and put your scss files in the assets directory. +``` +from flask_scss import Scss +Scss(app,static_dir='app/static', asset_dir='app/assets') + +``` +## deploy to heroku +``` +git push heroku deploy:master +``` +## db changes +``` +flask db migrate -m "Your migration note" +flask db upgrade +``` diff --git a/requirements.txt b/requirements.txt index 1d1d743..8c8104f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,36 @@ alembic==1.0.0 blinker==1.4 +boto3==1.9.0 +botocore==1.12.0 click==6.7 +docutils==0.14 dominate==2.3.1 Flask==1.0.2 Flask-Bootstrap==3.3.7.1 Flask-Login==0.4.1 Flask-Mail==0.9.1 Flask-Migrate==2.2.1 +Flask-Scss==0.5 Flask-SQLAlchemy==2.3.2 +Flask-Uploads==0.2.1 Flask-WTF==0.14.2 gunicorn==19.9.0 itsdangerous==0.24 Jinja2==2.10 +jmespath==0.9.3 +libsass==0.14.5 Mako==1.0.7 MarkupSafe==1.0 psycopg2==2.7.5 PyJWT==1.6.4 +pyScss==1.3.5 python-dateutil==2.7.3 python-dotenv==0.9.1 python-editor==1.0.3 +s3transfer==0.1.13 six==1.11.0 SQLAlchemy==1.2.10 +urllib3==1.23 visitor==0.1.3 Werkzeug==0.14.1 -WTForms==2.2.1 +WTForms==2.2.1 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 27a3e28..0000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - setup_requires=['libsass >= 0.6.0'], - sass_manifests={ - 'app': ('static\scss', 'static\css', '\static\css') - } -) \ No newline at end of file