diff --git a/alembic/env.py b/alembic/env.py index bad6e54..36958d9 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -76,4 +76,3 @@ def run_migrations_online(): run_migrations_offline() else: run_migrations_online() - diff --git a/alembic/versions/29c2cf7cd05f_add_comments_to_even.py b/alembic/versions/29c2cf7cd05f_add_comments_to_even.py new file mode 100644 index 0000000..8e45bfe --- /dev/null +++ b/alembic/versions/29c2cf7cd05f_add_comments_to_even.py @@ -0,0 +1,22 @@ +"""Add comments to events + +Revision ID: 29c2cf7cd05f +Revises: 2e8783dd05 +Create Date: 2013-04-19 21:29:32.485229 + +""" + +# revision identifiers, used by Alembic. +revision = '29c2cf7cd05f' +down_revision = '2e8783dd05' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('event', sa.Column('comments_id', sa.Integer, sa.ForeigenKey('commentspace.id'))) + + +def downgrade(): + op.remove_column('event', 'comments_id') diff --git a/hacknight/models/__init__.py b/hacknight/models/__init__.py index daebcb6..e720907 100644 --- a/hacknight/models/__init__.py +++ b/hacknight/models/__init__.py @@ -6,9 +6,11 @@ db = SQLAlchemy(app) -from hacknight.models.user import * from hacknight.models.event import * +from hacknight.models.user import * +from hacknight.models.profile import * from hacknight.models.venue import * from hacknight.models.project import * from hacknight.models.participant import * from hacknight.models.sponsor import * +from hacknight.models.comment import * diff --git a/hacknight/models/comment.py b/hacknight/models/comment.py index fb0e744..748b41a 100644 --- a/hacknight/models/comment.py +++ b/hacknight/models/comment.py @@ -1,11 +1,9 @@ # -*- coding: utf-8- *- -from hacknight.models import BaseMixin, BaseScopedIdNameMixin, BaseScopedIdMixin +from hacknight.models import BaseMixin, BaseScopedIdMixin from hacknight.models import db -from hacknight.models.event import Event -from hacknight.models.participant import Participant from hacknight.models.user import User -from hacknight.models.vote import Vote, VoteSpace +from hacknight.models.vote import VoteSpace __all__ = ['CommentSpace', 'Comment'] diff --git a/hacknight/models/event.py b/hacknight/models/event.py index 8441d44..afb3b04 100644 --- a/hacknight/models/event.py +++ b/hacknight/models/event.py @@ -3,9 +3,18 @@ from flask import url_for from flask.ext.lastuser.sqlalchemy import ProfileMixin from sqlalchemy.orm import deferred +<<<<<<< HEAD +from hacknight.models import db, BaseScopedNameMixin +from hacknight.models.profile import Profile +from hacknight.models.comment import CommentSpace + + +__all__ = ['Event', 'EVENT_STATUS'] +======= from hacknight.models import db, BaseNameMixin, BaseScopedNameMixin, BaseMixin __all__ = ['Profile', 'Event', 'EVENT_STATUS', 'PROFILE_TYPE', 'EventRedirect'] +>>>>>>> master #need to add EventTurnOut, EventPayment later @@ -34,6 +43,8 @@ class EVENT_STATUS: WITHDRAWN = 7 +<<<<<<< HEAD +======= class Profile(ProfileMixin, BaseNameMixin, db.Model): __tablename__ = 'profile' @@ -51,8 +62,10 @@ def url_for(self, action='view', _external=True): return url_for('event_new', profile=self.name, _external=_external) +>>>>>>> master class Event(BaseScopedNameMixin, db.Model): __tablename__ = 'event' + profile_id = db.Column(db.Integer, db.ForeignKey('profile.id'), nullable=False) profile = db.relationship(Profile) parent = db.synonym('profile') @@ -76,8 +89,17 @@ class Event(BaseScopedNameMixin, db.Model): pending_message = deferred(db.Column(db.UnicodeText, nullable=False, default=u'')) pending_message_text = deferred(db.Column(db.UnicodeText, nullable=False, default=u'')) + #event wall + comments_id = db.Column(db.Integer, db.ForeignKey('commentspace.id'), nullable=False) + comments = db.relationship(CommentSpace, uselist=False) + __table_args__ = (db.UniqueConstraint('name', 'profile_id'),) + def __init__(self, **kwargs): + super(Event, self).__init__(**kwargs) + if not self.comments: + self.comments = CommentSpace() + def owner_is(self, user): """Check if a user is an owner of this event""" return user is not None and self.profile.userid in user.user_organizations_owned_ids() diff --git a/hacknight/models/profile.py b/hacknight/models/profile.py new file mode 100644 index 0000000..31dc04a --- /dev/null +++ b/hacknight/models/profile.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from flask import url_for +from hacknight.models import db, BaseNameMixin + +__all__ = ['Profile', 'PROFILE_TYPE'] + + +class PROFILE_TYPE: + UNDEFINED = 0 + PERSON = 1 + ORGANIZATION = 2 + EVENTSERIES = 3 + +profile_types = { + 0: u"Undefined", + 1: u"Person", + 2: u"Organization", + 3: u"Event Series", + } + + +class Profile(BaseNameMixin, db.Model): + __tablename__ = 'profile' + + userid = db.Column(db.Unicode(22), nullable=False, unique=True) + description = db.Column(db.UnicodeText, default=u'', nullable=False) + type = db.Column(db.Integer, default=PROFILE_TYPE.UNDEFINED, nullable=False) + + def type_label(self): + return profile_types.get(self.type, profile_types[0]) + + def url_for(self, action='view', _external=True): + if action == 'view': + return url_for('profile_view', profile=self.name, _external=_external) + elif action == 'new-event': + return url_for('event_new', profile=self.name, _external=_external) diff --git a/hacknight/models/user.py b/hacknight/models/user.py index b757a22..dd115c6 100644 --- a/hacknight/models/user.py +++ b/hacknight/models/user.py @@ -3,7 +3,7 @@ from flask import url_for from flask.ext.lastuser.sqlalchemy import UserBase from hacknight.models import db -from hacknight.models.event import Profile +from hacknight.models.profile import Profile __all__ = ['User'] diff --git a/hacknight/models/venue.py b/hacknight/models/venue.py index af1ae74..2a4a2e6 100644 --- a/hacknight/models/venue.py +++ b/hacknight/models/venue.py @@ -3,7 +3,7 @@ from flask import url_for from hacknight.models import BaseNameMixin from hacknight.models import db -from hacknight.models.event import Profile +from hacknight.models.profile import Profile __all__ = ['Venue'] diff --git a/hacknight/models/vote.py b/hacknight/models/vote.py index b5eea3e..fcaf73a 100644 --- a/hacknight/models/vote.py +++ b/hacknight/models/vote.py @@ -1,13 +1,13 @@ # -*- coding: utf-8- *- -from hacknight.models import BaseMixin, BaseScopedIdNameMixin +from hacknight.models import BaseMixin from hacknight.models import db -from hacknight.models.event import Event -from hacknight.models.participant import Participant from hacknight.models.user import User + __all__ = ['VoteSpace', 'Vote'] + class VoteSpace(BaseMixin, db.Model): __tablename__ = 'votespace' type = db.Column(db.Integer, nullable=True) diff --git a/hacknight/templates/comment_owner_email.md b/hacknight/templates/comment_owner_email.md index 6d0a33a..407eab5 100644 --- a/hacknight/templates/comment_owner_email.md +++ b/hacknight/templates/comment_owner_email.md @@ -1,4 +1,4 @@ -**{{ g.user.username }}** replied to you in the project **{{ project.title }}** +**{{ g.user.username }}** replied to you {% if wall %} on the event wall {% else %} in the project {% endif %} **{{ project.title }}** {{ comment.message }} diff --git a/hacknight/templates/event.html b/hacknight/templates/event.html index dab0a3e..de63acf 100644 --- a/hacknight/templates/event.html +++ b/hacknight/templates/event.html @@ -1,4 +1,7 @@ {% extends "layout.html" %} +{% from "comments.html" import commenttree %} +{% from "forms.html" import renderform, ajaxform %} + {% block title %}{{ event.title }}{% endblock %} {% macro participant_list(event, participants) -%} @@ -142,6 +145,7 @@
@@ -192,6 +196,55 @@

New project...

+
+
+
+
Discussion space for Hacknight +

Comments

+ {% if comments %} +
    + {{ commenttree(comments, event, g.user, request.base_url) }} +
+ {% endif %} + {% if not g.user -%} +

+ Login with Twitter or Google to leave a comment → +

+ {% else -%} + +
+ +

+ {{ commentform.message() }} +

+

+ +

+
+ + {% endif %} +
+
+
+
{% if event.venue.latitude and event.venue.longitude %}
@@ -235,7 +288,8 @@

New project...

function onZoomend(){ map.setView(venue, map.getZoom()); }; - {% endif %} + {% endif %} + commentsInit("{{ request.base_url }}"); }); diff --git a/hacknight/templates/project_owner_email.md b/hacknight/templates/project_owner_email.md index 0587e14..4f7c7ff 100644 --- a/hacknight/templates/project_owner_email.md +++ b/hacknight/templates/project_owner_email.md @@ -1,4 +1,4 @@ -**{{ g.user.username }}** left a comment on your project **{{ project.title }}** +**{{ g.user.username }}** left a comment {% if wall %} on the event wall {% else %} on your project {% endif %} **{{ project.title }}** {{ comment.message }} diff --git a/hacknight/templates/project_team_email.md b/hacknight/templates/project_team_email.md index d9fce8a..0e50a37 100644 --- a/hacknight/templates/project_team_email.md +++ b/hacknight/templates/project_team_email.md @@ -1,4 +1,4 @@ -**{{ g.user.username }}** left a comment in the project **{{ project.title }}** +**{{ g.user.username }}** left a comment {% if wall %} on the event wall {% else %} in the project {% endif %} **{{ project.title }}** {{ comment.message }} diff --git a/hacknight/views/event.py b/hacknight/views/event.py index 7b242ee..b851b40 100644 --- a/hacknight/views/event.py +++ b/hacknight/views/event.py @@ -1,18 +1,24 @@ # -*- coding: utf-8 -*- +from datetime import datetime from sqlalchemy.orm import joinedload from sqlalchemy import func from html2text import html2text from flask.ext.mail import Message -from flask import render_template, abort, flash, url_for, g, request, Markup -from coaster.views import load_model, load_models +from flask import render_template, abort, flash, url_for, g, request, Markup, redirect +from coaster.views import load_model, load_models, jsonp from baseframe.forms import render_redirect, render_form, render_delete_sqla from hacknight import app, mail -from hacknight.models import db, Profile, Event, User, Participant, PARTICIPANT_STATUS, EventRedirect +from hacknight.models import db, Profile, Event, User, Participant, PARTICIPANT_STATUS, EventRedirect, Comment from hacknight.forms.event import EventForm, ConfirmWithdrawForm, SendEmailForm, EmailEventParticipantsForm from hacknight.forms.participant import ParticipantForm +from hacknight.forms.comment import CommentForm, DeleteCommentForm from hacknight.views.login import lastuser from hacknight.views.workflow import ParticipantWorkflow +from markdown import Markdown +import bleach + +markdown = Markdown(safe_mode="escape").convert #map participant status event template @@ -33,7 +39,7 @@ def send_email(sender, to, subject, body, html=None): mail.send(msg) -@app.route('//', methods=["GET"]) +@app.route('//', methods=["GET", "POST"]) @load_models( (Profile, {'name': 'profile'}, 'profile'), (Event, {'name': 'event', 'profile': 'profile'}, 'event')) @@ -53,6 +59,51 @@ def event_view(profile, event): applied = True break current_participant = Participant.get(user=g.user, event=event) if g.user else None + comments = sorted(Comment.query.filter_by(commentspace=event.comments, reply_to=None).order_by('created_at').all(), + key=lambda c: c.votes.count, reverse=True) + commentform = CommentForm() + delcommentform = DeleteCommentForm() + commentspace = event.comments + if request.method == 'POST': + if request.form.get('form.id') == 'newcomment' and commentform.validate(): + if commentform.edit_id.data: + comment = commentspace.get_comment(int(commentform.edit_id.data)) + if comment: + if comment.user == g.user: + comment.message = commentform.message.data + comment.message_html = markdown(comment.message) + comment.edited_at = datetime.utcnow() + flash("Your comment has been edited", "info") + else: + flash("You can only edit your own comments", "info") + else: + flash("No such comment", "error") + else: + comment = Comment(user=g.user, commentspace=event.comments, message=commentform.message.data) + comment.message_html = bleach.linkify(markdown(commentform.message.data)) + event.comments.count += 1 + comment.votes.vote(g.user) # Vote for your own comment + comment.make_id() + db.session.add(comment) + flash("Your comment has been posted", "info") + db.session.commit() + # Redirect despite this being the same page because HTTP 303 is required to not break + # the browser Back button + return redirect(event.url_for() + "#wall") + + elif request.form.get('form.id') == 'delcomment' and delcommentform.validate(): + comment = commentspace.get_comment(int(delcommentform.comment_id.data)) + if comment: + if comment.user == g.user: + comment.delete() + event.comments.count -= 1 + db.session.commit() + flash("Your comment was deleted.", "info") + else: + flash("You did not post that comment.", "error") + else: + flash("No such comment.", "error") + return redirect(event.url_for() + "#wall") return render_template('event.html', profile=profile, event=event, projects=event.projects, accepted_participants=accepted_participants, @@ -60,7 +111,60 @@ def event_view(profile, event): applied=applied, current_participant=current_participant, sponsors=event.sponsors, - workflow=workflow) + comments=comments, + commentform=commentform, + delcommentform=delcommentform) + + +@app.route('///comments//voteup', methods=['GET', 'POST']) +@lastuser.requires_login +@load_models( + (Profile, {'name': 'profile'}, 'profile'), + (Event, {'name': 'event', 'profile': 'profile'}, 'event'), + (Comment, {'url_id': 'cid', 'commentspace': 'event.comments'}, 'comment')) +def wall_voteupcomment(profile, event, comment): + comment.votes.vote(g.user, votedown=False) + db.session.commit() + flash("Your vote has been recorded", "info") + return redirect(event.url_for() + "#wall") + + +@app.route('///comments//votedown', methods=['GET', 'POST']) +@lastuser.requires_login +@load_models( + (Profile, {'name': 'profile'}, 'profile'), + (Event, {'name': 'event', 'profile': 'profile'}, 'event'), + (Comment, {'url_id': 'cid', 'commentspace': 'event.comments'}, 'comment')) +def wall_votedowncomment(profile, event, comment): + comment.votes.vote(g.user, votedown=True) + db.session.commit() + flash("Your vote has been recorded", "info") + return redirect(event.url_for() + "#wall", code=302) + + +@app.route('///comments//json') +@load_models( + (Profile, {'name': 'profile'}, 'profile'), + (Event, {'name': 'event', 'profile': 'profile'}, 'event'), + (Comment, {'url_id': 'cid', 'commentspace': 'event.comments'}, 'comment')) +def wall_jsoncomment(profile, event, comment): + # comment = Comment.query.get(cid) + if comment: + return jsonp(message=comment.message) + return jsonp(message='') + + +@app.route('///comments//cancelvote', methods=['GET', 'POST']) +@lastuser.requires_login +@load_models( + (Profile, {'name': 'profile'}, 'profile'), + (Event, {'name': 'event', 'profile': 'profile'}, 'event'), + (Comment, {'url_id': 'cid', 'commentspace': 'event.comments'}, 'comment')) +def wall_votecancelcomment(profile, event, comment): + comment.votes.cancelvote(g.user) + db.session.commit() + flash("Your vote has been withdrawn", "info") + return redirect(event.url_for() + "#wall", code=302) @app.route('//new', methods=['GET', 'POST']) @@ -163,6 +267,20 @@ def event_update_participant_status(profile, event): if participant.status == PARTICIPANT_STATUS.WITHDRAWN: abort(403) if participant.status != status: +<<<<<<< HEAD + participant.status = status + try: + text_message = getattr(event, (participants_email_attrs[status] + '_text')) + text_message = text_message.replace("*|FULLNAME|*", participant.user.fullname) + message = getattr(event, participants_email_attrs[status]) + message = message.replace("*|FULLNAME|*", participant.user.fullname) + if message: + send_email(sender=(g.user.fullname, g.user.email), to=participant.email, + subject="%s - Hacknight participation status" % event.title , body=text_message, html=message) + except KeyError: + pass + db.session.commit() +======= if event.confirmed_participants_count() < event.maximum_participants: participant.status = status try: @@ -178,6 +296,7 @@ def event_update_participant_status(profile, event): db.session.commit() else: flash("Venue capacity is full", "error") +>>>>>>> master return "Done" abort(403) diff --git a/hacknight/views/project.py b/hacknight/views/project.py index 87b4a31..714bdc0 100644 --- a/hacknight/views/project.py +++ b/hacknight/views/project.py @@ -217,7 +217,7 @@ def project_view(profile, event, project): db.session.commit() link = project.url_for("view", _external=True) + "#c" + str(comment.id) for item in send_email_info: - email_body = render_template(item.pop('template'), project=project, comment=comment, link=link) + email_body = render_template(item.pop('template'), project=project, comment=comment, wall=False, link=link) if item['to']: send_email(sender=None, html=markdown(email_body), body=email_body, **item) # Redirect despite this being the same page because HTTP 303 is required to not break