New notifications
- Added request.notifications - Email configuration fixes - Set config_spec default SMTP port to `0` and switch to SSL/non-SSL default if `port == 0` - Added email_smtp_use_ssl configuration setting - Added migrations for notification tables - Added __repr__ to MediaComment(Mixin) - Added MediaComment.get_entry => MediaEntry - Added CommentSubscription, CommentNotification, Notification, ProcessingNotification tables - Added notifications.task to celery init - Fixed a bug in the video transcoder where pygst would hijack the --help argument. - Added notifications - views - silence - subscribe - routes - utility methods - celery task - Added half-hearted .active comment CSS style - Added quick JS to show header_dropdown - Added fragment template to show notifications in header_dropdown - Added fragment template to show subscribe/unsubscribe buttons on media/comment pages - Updated celery setup tests with notifications.task - Tried to fix test_misc tests that I broke - Added notification tests - Added and extended tests.tools fixtures - Integrated new notifications into media_home, media_post_comment views - Bumped SQLAlchemy dependency to >= 0.8.0 since we need polymorphic for the notifications to work
This commit is contained in:
parent
25aad338d4
commit
2d7b6bdef9
@ -37,6 +37,7 @@ from mediagoblin.init import (get_jinja_loader, get_staticdirector,
|
||||
setup_storage)
|
||||
from mediagoblin.tools.pluginapi import PluginManager, hook_transform
|
||||
from mediagoblin.tools.crypto import setup_crypto
|
||||
from mediagoblin import notifications
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@ -186,6 +187,8 @@ class MediaGoblinApp(object):
|
||||
|
||||
request.urlgen = build_proxy
|
||||
|
||||
request.notifications = notifications
|
||||
|
||||
mg_request.setup_user_in_request(request)
|
||||
|
||||
request.controller_name = None
|
||||
|
@ -22,9 +22,10 @@ direct_remote_path = string(default="/mgoblin_static/")
|
||||
|
||||
# set to false to enable sending notices
|
||||
email_debug_mode = boolean(default=True)
|
||||
email_smtp_use_ssl = boolean(default=False)
|
||||
email_sender_address = string(default="notice@mediagoblin.example.org")
|
||||
email_smtp_host = string(default='')
|
||||
email_smtp_port = integer(default=25)
|
||||
email_smtp_port = integer(default=0)
|
||||
email_smtp_user = string(default=None)
|
||||
email_smtp_pass = string(default=None)
|
||||
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.sql import and_
|
||||
from migrate.changeset.constraint import UniqueConstraint
|
||||
|
||||
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
|
||||
from mediagoblin.db.models import MediaEntry, Collection, User
|
||||
from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
@ -287,3 +287,58 @@ def unique_collections_slug(db):
|
||||
constraint.create()
|
||||
|
||||
db.commit()
|
||||
|
||||
class CommentSubscription_v0(declarative_base()):
|
||||
__tablename__ = 'core__comment_subscriptions'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
|
||||
media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
|
||||
|
||||
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
|
||||
|
||||
notify = Column(Boolean, nullable=False, default=True)
|
||||
send_email = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
|
||||
class Notification_v0(declarative_base()):
|
||||
__tablename__ = 'core__notifications'
|
||||
id = Column(Integer, primary_key=True)
|
||||
type = Column(Unicode)
|
||||
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
|
||||
user_id = Column(Integer, ForeignKey(User.id), nullable=False,
|
||||
index=True)
|
||||
seen = Column(Boolean, default=lambda: False, index=True)
|
||||
|
||||
|
||||
class CommentNotification_v0(Notification_v0):
|
||||
__tablename__ = 'core__comment_notifications'
|
||||
id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
|
||||
|
||||
subject_id = Column(Integer, ForeignKey(MediaComment.id))
|
||||
|
||||
|
||||
class ProcessingNotification_v0(Notification_v0):
|
||||
__tablename__ = 'core__processing_notifications'
|
||||
|
||||
id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
|
||||
|
||||
subject_id = Column(Integer, ForeignKey(MediaEntry.id))
|
||||
|
||||
|
||||
@RegisterMigration(11, MIGRATIONS)
|
||||
def add_new_notification_tables(db):
|
||||
metadata = MetaData(bind=db.bind)
|
||||
|
||||
user_table = inspect_table(metadata, 'core__users')
|
||||
mediaentry_table = inspect_table(metadata, 'core__media_entries')
|
||||
mediacomment_table = inspect_table(metadata, 'core__media_comments')
|
||||
|
||||
CommentSubscription_v0.__table__.create(db.bind)
|
||||
|
||||
Notification_v0.__table__.create(db.bind)
|
||||
CommentNotification_v0.__table__.create(db.bind)
|
||||
ProcessingNotification_v0.__table__.create(db.bind)
|
||||
|
@ -31,6 +31,8 @@ import uuid
|
||||
import re
|
||||
import datetime
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
@ -288,6 +290,13 @@ class MediaCommentMixin(object):
|
||||
"""
|
||||
return cleaned_markdown_conversion(self.content)
|
||||
|
||||
def __repr__(self):
|
||||
return '<{klass} #{id} {author} "{comment}">'.format(
|
||||
klass=self.__class__.__name__,
|
||||
id=self.id,
|
||||
author=self.get_author,
|
||||
comment=self.content)
|
||||
|
||||
|
||||
class CollectionMixin(GenerateSlugMixin):
|
||||
def check_slug_used(self, slug):
|
||||
|
@ -24,15 +24,17 @@ import datetime
|
||||
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
||||
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
||||
SmallInteger
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
from sqlalchemy.orm import relationship, backref, with_polymorphic
|
||||
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||
from sqlalchemy.sql.expression import desc
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.util import memoized_property
|
||||
|
||||
|
||||
from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
|
||||
from mediagoblin.db.base import Base, DictReadAttrProxy
|
||||
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
|
||||
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
|
||||
MediaCommentMixin, CollectionMixin, CollectionItemMixin
|
||||
from mediagoblin.tools.files import delete_media_files
|
||||
from mediagoblin.tools.common import import_component
|
||||
|
||||
@ -60,9 +62,9 @@ class User(Base, UserMixin):
|
||||
# the RFC) and because it would be a mess to implement at this
|
||||
# point.
|
||||
email = Column(Unicode, nullable=False)
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
pw_hash = Column(Unicode, nullable=False)
|
||||
email_verified = Column(Boolean, default=False)
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
status = Column(Unicode, default=u"needs_email_verification", nullable=False)
|
||||
# Intented to be nullable=False, but migrations would not work for it
|
||||
# set to nullable=True implicitly.
|
||||
@ -392,6 +394,10 @@ class MediaComment(Base, MediaCommentMixin):
|
||||
backref=backref("posted_comments",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan"))
|
||||
get_entry = relationship(MediaEntry,
|
||||
backref=backref("comments",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan"))
|
||||
|
||||
# Cascade: Comments are somewhat owned by their MediaEntry.
|
||||
# So do the full thing.
|
||||
@ -484,9 +490,103 @@ class ProcessingMetaData(Base):
|
||||
return DictReadAttrProxy(self)
|
||||
|
||||
|
||||
class CommentSubscription(Base):
|
||||
__tablename__ = 'core__comment_subscriptions'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
|
||||
media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
|
||||
media_entry = relationship(MediaEntry,
|
||||
backref=backref('comment_subscriptions',
|
||||
cascade='all, delete-orphan'))
|
||||
|
||||
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
|
||||
user = relationship(User,
|
||||
backref=backref('comment_subscriptions',
|
||||
cascade='all, delete-orphan'))
|
||||
|
||||
notify = Column(Boolean, nullable=False, default=True)
|
||||
send_email = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
def __repr__(self):
|
||||
return ('<{classname} #{id}: {user} {media} notify: '
|
||||
'{notify} email: {email}>').format(
|
||||
id=self.id,
|
||||
classname=self.__class__.__name__,
|
||||
user=self.user,
|
||||
media=self.media_entry,
|
||||
notify=self.notify,
|
||||
email=self.send_email)
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = 'core__notifications'
|
||||
id = Column(Integer, primary_key=True)
|
||||
type = Column(Unicode)
|
||||
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
|
||||
user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
|
||||
index=True)
|
||||
seen = Column(Boolean, default=lambda: False, index=True)
|
||||
user = relationship(
|
||||
User,
|
||||
backref=backref('notifications', cascade='all, delete-orphan'))
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'notification',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
|
||||
id=self.id,
|
||||
klass=self.__class__.__name__,
|
||||
user=self.user,
|
||||
subject=getattr(self, 'subject', None),
|
||||
seen='unseen' if not self.seen else 'seen')
|
||||
|
||||
|
||||
class CommentNotification(Notification):
|
||||
__tablename__ = 'core__comment_notifications'
|
||||
id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
|
||||
|
||||
subject_id = Column(Integer, ForeignKey(MediaComment.id))
|
||||
subject = relationship(
|
||||
MediaComment,
|
||||
backref=backref('comment_notifications', cascade='all, delete-orphan'))
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'comment_notification'
|
||||
}
|
||||
|
||||
|
||||
class ProcessingNotification(Notification):
|
||||
__tablename__ = 'core__processing_notifications'
|
||||
|
||||
id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
|
||||
|
||||
subject_id = Column(Integer, ForeignKey(MediaEntry.id))
|
||||
subject = relationship(
|
||||
MediaEntry,
|
||||
backref=backref('processing_notifications',
|
||||
cascade='all, delete-orphan'))
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'processing_notification'
|
||||
}
|
||||
|
||||
|
||||
with_polymorphic(
|
||||
Notification,
|
||||
[ProcessingNotification, CommentNotification])
|
||||
|
||||
MODELS = [
|
||||
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
|
||||
MediaAttachmentFile, ProcessingMetaData]
|
||||
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
|
||||
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
|
||||
Notification, CommentNotification, ProcessingNotification,
|
||||
CommentSubscription]
|
||||
|
||||
|
||||
######################################################
|
||||
|
@ -16,12 +16,18 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from celery import Celery
|
||||
from mediagoblin.tools.pluginapi import hook_runall
|
||||
|
||||
|
||||
MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task']
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MANDATORY_CELERY_IMPORTS = [
|
||||
'mediagoblin.processing.task',
|
||||
'mediagoblin.notifications.task']
|
||||
|
||||
DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
|
||||
|
||||
@ -97,3 +103,13 @@ def setup_celery_from_config(app_config, global_config,
|
||||
|
||||
if set_environ:
|
||||
os.environ['CELERY_CONFIG_MODULE'] = settings_module
|
||||
|
||||
# Replace the default celery.current_app.conf if celery has already been
|
||||
# initiated
|
||||
from celery import current_app
|
||||
|
||||
_log.info('Setting celery configuration from object "{0}"'.format(
|
||||
settings_module))
|
||||
current_app.config_from_object(this_module)
|
||||
|
||||
_log.debug('Celery broker host: {0}'.format(current_app.conf['BROKER_HOST']))
|
||||
|
@ -46,7 +46,7 @@ def sniff_handler(media_file, **kw):
|
||||
if kw.get('media') is not None:
|
||||
name, ext = os.path.splitext(kw['media'].filename)
|
||||
clean_ext = ext[1:].lower()
|
||||
|
||||
|
||||
if clean_ext in SUPPORTED_FILETYPES:
|
||||
_log.info('Found file extension in supported filetypes')
|
||||
return True
|
||||
|
@ -22,9 +22,15 @@ import logging
|
||||
import urllib
|
||||
import multiprocessing
|
||||
import gobject
|
||||
|
||||
old_argv = sys.argv
|
||||
sys.argv = []
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
sys.argv = old_argv
|
||||
import struct
|
||||
try:
|
||||
from PIL import Image
|
||||
|
141
mediagoblin/notifications/__init__.py
Normal file
141
mediagoblin/notifications/__init__.py
Normal file
@ -0,0 +1,141 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from mediagoblin.db.models import Notification, \
|
||||
CommentNotification, CommentSubscription
|
||||
from mediagoblin.notifications.task import email_notification_task
|
||||
from mediagoblin.notifications.tools import generate_comment_message
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
def trigger_notification(comment, media_entry, request):
|
||||
'''
|
||||
Send out notifications about a new comment.
|
||||
'''
|
||||
subscriptions = CommentSubscription.query.filter_by(
|
||||
media_entry_id=media_entry.id).all()
|
||||
|
||||
for subscription in subscriptions:
|
||||
if not subscription.notify:
|
||||
continue
|
||||
|
||||
if comment.get_author == subscription.user:
|
||||
continue
|
||||
|
||||
cn = CommentNotification(
|
||||
user_id=subscription.user_id,
|
||||
subject_id=comment.id)
|
||||
|
||||
cn.save()
|
||||
|
||||
if subscription.send_email:
|
||||
message = generate_comment_message(
|
||||
subscription.user,
|
||||
comment,
|
||||
media_entry,
|
||||
request)
|
||||
|
||||
email_notification_task.apply_async([cn.id, message])
|
||||
|
||||
|
||||
def mark_notification_seen(notification):
|
||||
if notification:
|
||||
notification.seen = True
|
||||
notification.save()
|
||||
|
||||
|
||||
def mark_comment_notification_seen(comment_id, user):
|
||||
notification = CommentNotification.query.filter_by(
|
||||
user_id=user.id,
|
||||
subject_id=comment_id).first()
|
||||
|
||||
_log.debug('Marking {0} as seen.'.format(notification))
|
||||
|
||||
mark_notification_seen(notification)
|
||||
|
||||
|
||||
def get_comment_subscription(user_id, media_entry_id):
|
||||
return CommentSubscription.query.filter_by(
|
||||
user_id=user_id,
|
||||
media_entry_id=media_entry_id).first()
|
||||
|
||||
def add_comment_subscription(user, media_entry):
|
||||
'''
|
||||
Create a comment subscription for a User on a MediaEntry.
|
||||
|
||||
Uses the User's wants_comment_notification to set email notifications for
|
||||
the subscription to enabled/disabled.
|
||||
'''
|
||||
cn = get_comment_subscription(user.id, media_entry.id)
|
||||
|
||||
if not cn:
|
||||
cn = CommentSubscription(
|
||||
user_id=user.id,
|
||||
media_entry_id=media_entry.id)
|
||||
|
||||
cn.notify = True
|
||||
|
||||
if not user.wants_comment_notification:
|
||||
cn.send_email = False
|
||||
|
||||
cn.save()
|
||||
|
||||
|
||||
def silence_comment_subscription(user, media_entry):
|
||||
'''
|
||||
Silence a subscription so that the user is never notified in any way about
|
||||
new comments on an entry
|
||||
'''
|
||||
cn = get_comment_subscription(user.id, media_entry.id)
|
||||
|
||||
if cn:
|
||||
cn.notify = False
|
||||
cn.send_email = False
|
||||
cn.save()
|
||||
|
||||
|
||||
def remove_comment_subscription(user, media_entry):
|
||||
cn = get_comment_subscription(user.id, media_entry.id)
|
||||
|
||||
if cn:
|
||||
cn.delete()
|
||||
|
||||
|
||||
NOTIFICATION_FETCH_LIMIT = 100
|
||||
|
||||
|
||||
def get_notifications(user_id, only_unseen=True):
|
||||
query = Notification.query.filter_by(user_id=user_id)
|
||||
|
||||
if only_unseen:
|
||||
query = query.filter_by(seen=False)
|
||||
|
||||
notifications = query.limit(
|
||||
NOTIFICATION_FETCH_LIMIT).all()
|
||||
|
||||
return notifications
|
||||
|
||||
def get_notification_count(user_id, only_unseen=True):
|
||||
query = Notification.query.filter_by(user_id=user_id)
|
||||
|
||||
if only_unseen:
|
||||
query = query.filter_by(seen=False)
|
||||
|
||||
count = query.count()
|
||||
|
||||
return count
|
25
mediagoblin/notifications/routing.py
Normal file
25
mediagoblin/notifications/routing.py
Normal file
@ -0,0 +1,25 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.tools.routing import add_route
|
||||
|
||||
add_route('mediagoblin.notifications.subscribe_comments',
|
||||
'/u/<string:user>/m/<string:media>/notifications/subscribe/comments/',
|
||||
'mediagoblin.notifications.views:subscribe_comments')
|
||||
|
||||
add_route('mediagoblin.notifications.silence_comments',
|
||||
'/u/<string:user>/m/<string:media>/notifications/silence/',
|
||||
'mediagoblin.notifications.views:silence_comments')
|
46
mediagoblin/notifications/task.py
Normal file
46
mediagoblin/notifications/task.py
Normal file
@ -0,0 +1,46 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from celery import registry
|
||||
from celery.task import Task
|
||||
|
||||
from mediagoblin.tools.mail import send_email
|
||||
from mediagoblin.db.models import CommentNotification
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailNotificationTask(Task):
|
||||
'''
|
||||
Celery notification task.
|
||||
|
||||
This task is executed by celeryd to offload long-running operations from
|
||||
the web server.
|
||||
'''
|
||||
def run(self, notification_id, message):
|
||||
cn = CommentNotification.query.filter_by(id=notification_id).first()
|
||||
_log.info('Sending notification email about {0}'.format(cn))
|
||||
|
||||
return send_email(
|
||||
message['from'],
|
||||
[message['to']],
|
||||
message['subject'],
|
||||
message['body'])
|
||||
|
||||
email_notification_task = registry.tasks[EmailNotificationTask.name]
|
55
mediagoblin/notifications/tools.py
Normal file
55
mediagoblin/notifications/tools.py
Normal file
@ -0,0 +1,55 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.tools.template import render_template
|
||||
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||
from mediagoblin import mg_globals
|
||||
|
||||
def generate_comment_message(user, comment, media, request):
|
||||
"""
|
||||
Sends comment email to user when a comment is made on their media.
|
||||
|
||||
Args:
|
||||
- user: the user object to whom the email is sent
|
||||
- comment: the comment object referencing user's media
|
||||
- media: the media object the comment is about
|
||||
- request: the request
|
||||
"""
|
||||
|
||||
comment_url = request.urlgen(
|
||||
'mediagoblin.user_pages.media_home.view_comment',
|
||||
comment=comment.id,
|
||||
user=media.get_uploader.username,
|
||||
media=media.slug_or_id,
|
||||
qualified=True) + '#comment'
|
||||
|
||||
comment_author = comment.get_author.username
|
||||
|
||||
rendered_email = render_template(
|
||||
request, 'mediagoblin/user_pages/comment_email.txt',
|
||||
{'username': user.username,
|
||||
'comment_author': comment_author,
|
||||
'comment_content': comment.content,
|
||||
'comment_url': comment_url})
|
||||
|
||||
return {
|
||||
'from': mg_globals.app_config['email_sender_address'],
|
||||
'to': user.email,
|
||||
'subject': '{instance_title} - {comment_author} '.format(
|
||||
comment_author=comment_author,
|
||||
instance_title=mg_globals.app_config['html_title']) \
|
||||
+ _('commented on your post'),
|
||||
'body': rendered_email}
|
54
mediagoblin/notifications/views.py
Normal file
54
mediagoblin/notifications/views.py
Normal file
@ -0,0 +1,54 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.tools.response import render_to_response, render_404, redirect
|
||||
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
|
||||
get_media_entry_by_id,
|
||||
require_active_login, user_may_delete_media, user_may_alter_collection,
|
||||
get_user_collection, get_user_collection_item, active_user_from_url)
|
||||
|
||||
from mediagoblin import messages
|
||||
|
||||
from mediagoblin.notifications import add_comment_subscription, \
|
||||
silence_comment_subscription
|
||||
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
@get_user_media_entry
|
||||
@require_active_login
|
||||
def subscribe_comments(request, media):
|
||||
|
||||
add_comment_subscription(request.user, media)
|
||||
|
||||
messages.add_message(request,
|
||||
messages.SUCCESS,
|
||||
_('Subscribed to comments on %s!')
|
||||
% media.title)
|
||||
|
||||
return redirect(request, location=media.url_for_self(request.urlgen))
|
||||
|
||||
@get_user_media_entry
|
||||
@require_active_login
|
||||
def silence_comments(request, media):
|
||||
silence_comment_subscription(request.user, media)
|
||||
|
||||
messages.add_message(request,
|
||||
messages.SUCCESS,
|
||||
_('You will not receive notifications for comments on'
|
||||
' %s.') % media.title)
|
||||
|
||||
return redirect(request, location=media.url_for_self(request.urlgen))
|
@ -35,6 +35,7 @@ def get_url_map():
|
||||
import mediagoblin.edit.routing
|
||||
import mediagoblin.webfinger.routing
|
||||
import mediagoblin.listings.routing
|
||||
import mediagoblin.notifications.routing
|
||||
|
||||
for route in PluginManager().get_routes():
|
||||
add_route(*route)
|
||||
|
@ -384,6 +384,12 @@ a.comment_whenlink:hover {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.comment_active {
|
||||
box-shadow: 0px 0px 15px 15px #378566;
|
||||
background: #378566;
|
||||
color: #f7f7f7;
|
||||
}
|
||||
|
||||
textarea#comment_content {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
|
18
mediagoblin/static/js/notifications.js
Normal file
18
mediagoblin/static/js/notifications.js
Normal file
@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
var notifications = {};
|
||||
|
||||
(function (n) {
|
||||
n._base = '/';
|
||||
n._endpoint = 'notifications/json';
|
||||
|
||||
n.init = function () {
|
||||
$('.notification-gem').on('click', function () {
|
||||
$('.header_dropdown_down:visible').click();
|
||||
});
|
||||
}
|
||||
|
||||
})(notifications)
|
||||
|
||||
$(document).ready(function () {
|
||||
notifications.init();
|
||||
});
|
@ -34,6 +34,8 @@ from mediagoblin.media_types import sniff_media, \
|
||||
from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \
|
||||
run_process_media, new_upload_entry
|
||||
|
||||
from mediagoblin.notifications import add_comment_subscription
|
||||
|
||||
|
||||
@require_active_login
|
||||
def submit_start(request):
|
||||
@ -92,6 +94,8 @@ def submit_start(request):
|
||||
run_process_media(entry, feed_url)
|
||||
add_message(request, SUCCESS, _('Woohoo! Submitted!'))
|
||||
|
||||
add_comment_subscription(request.user, entry)
|
||||
|
||||
return redirect(request, "mediagoblin.user_pages.user_home",
|
||||
user=request.user.username)
|
||||
except Exception as e:
|
||||
|
@ -34,6 +34,8 @@
|
||||
src="{{ request.staticdirect('/js/extlib/jquery.js') }}"></script>
|
||||
<script type="text/javascript"
|
||||
src="{{ request.staticdirect('/js/header_dropdown.js') }}"></script>
|
||||
<script type="text/javascript"
|
||||
src="{{ request.staticdirect('/js/notifications.js') }}"></script>
|
||||
|
||||
{# For clarification, the difference between the extra_head.html template
|
||||
# and the head template hook is that the former should be used by
|
||||
@ -57,6 +59,9 @@
|
||||
<div class="header_right">
|
||||
{%- if request.user %}
|
||||
{% if request.user and request.user.status == 'active' %}
|
||||
|
||||
<a href="#notifications" class="notification-gem button_action" title="Notifications">
|
||||
{{ request.notifications.get_notification_count(request.user.id) }}</a>
|
||||
<div class="button_action header_dropdown_down">▼</div>
|
||||
<div class="button_action header_dropdown_up">▲</div>
|
||||
{% elif request.user and request.user.status == "needs_email_verification" %}
|
||||
@ -109,6 +114,7 @@
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% include 'mediagoblin/fragments/header_notifications.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
@ -0,0 +1,40 @@
|
||||
{% set notifications = request.notifications.get_notifications(request.user.id) %}
|
||||
{% if notifications %}
|
||||
<div class="header_notifications">
|
||||
<h3>{% trans %}New comments{% endtrans %}</h3>
|
||||
<ul>
|
||||
{% for notification in notifications %}
|
||||
{% set comment = notification.subject %}
|
||||
{% set comment_author = comment.get_author %}
|
||||
{% set media = comment.get_entry %}
|
||||
<li class="comment_wrapper">
|
||||
<div class="comment_author">
|
||||
<img src="{{ request.staticdirect('/images/icon_comment.png') }}" />
|
||||
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
|
||||
user=comment_author.username) }}"
|
||||
class="comment_authorlink">
|
||||
{{- comment_author.username -}}
|
||||
</a>
|
||||
<a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment',
|
||||
comment=comment.id,
|
||||
user=media.get_uploader.username,
|
||||
media=media.slug_or_id) }}#comment"
|
||||
class="comment_whenlink">
|
||||
<span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'>
|
||||
{%- trans formatted_time=timesince(comment.created) -%}
|
||||
{{ formatted_time }} ago
|
||||
{%- endtrans -%}
|
||||
</span>
|
||||
</a>:
|
||||
</div>
|
||||
<div class="comment_content">
|
||||
{% autoescape False -%}
|
||||
{{ comment.content_html }}
|
||||
{%- endautoescape %}
|
||||
</div>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
@ -167,6 +167,8 @@
|
||||
|
||||
{% include "mediagoblin/utils/exif.html" %}
|
||||
|
||||
{% include "mediagoblin/utils/comment-subscription.html" %}
|
||||
|
||||
{%- if media.attachment_files|count %}
|
||||
<h3>{% trans %}Attachments{% endtrans %}</h3>
|
||||
<ul>
|
||||
|
@ -0,0 +1,36 @@
|
||||
{#
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
{%- if request.user %}
|
||||
<p>
|
||||
{% set subscription = request.notifications.get_comment_subscription(
|
||||
request.user.id, media.id) %}
|
||||
{% if not subscription or not subscription.notify %}
|
||||
<a type="submit" href="{{ request.urlgen('mediagoblin.notifications.subscribe_comments',
|
||||
user=media.get_uploader.username,
|
||||
media=media.slug)}}"
|
||||
class="button_action">Subscribe to comments
|
||||
</a>
|
||||
{% else %}
|
||||
<a type="submit" href="{{ request.urlgen('mediagoblin.notifications.silence_comments',
|
||||
user=media.get_uploader.username,
|
||||
media=media.slug)}}"
|
||||
class="button_action">Silence comments
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{%- endif %}
|
@ -48,7 +48,7 @@ def test_setup_celery_from_config():
|
||||
assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float)
|
||||
assert fake_celery_module.CELERY_RESULT_PERSISTENT is True
|
||||
assert fake_celery_module.CELERY_IMPORTS == [
|
||||
'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task']
|
||||
'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task', 'mediagoblin.notifications.task']
|
||||
assert fake_celery_module.CELERY_RESULT_BACKEND == 'database'
|
||||
assert fake_celery_module.CELERY_RESULT_DBURI == (
|
||||
'sqlite:///' +
|
||||
|
@ -28,8 +28,10 @@ def test_user_deletes_other_comments(test_app):
|
||||
user_a = fixture_add_user(u"chris_a")
|
||||
user_b = fixture_add_user(u"chris_b")
|
||||
|
||||
media_a = fixture_media_entry(uploader=user_a.id, save=False)
|
||||
media_b = fixture_media_entry(uploader=user_b.id, save=False)
|
||||
media_a = fixture_media_entry(uploader=user_a.id, save=False,
|
||||
expunge=False)
|
||||
media_b = fixture_media_entry(uploader=user_b.id, save=False,
|
||||
expunge=False)
|
||||
Session.add(media_a)
|
||||
Session.add(media_b)
|
||||
Session.flush()
|
||||
@ -79,7 +81,7 @@ def test_user_deletes_other_comments(test_app):
|
||||
def test_media_deletes_broken_attachment(test_app):
|
||||
user_a = fixture_add_user(u"chris_a")
|
||||
|
||||
media = fixture_media_entry(uploader=user_a.id, save=False)
|
||||
media = fixture_media_entry(uploader=user_a.id, save=False, expunge=False)
|
||||
media.attachment_files.append(dict(
|
||||
name=u"some name",
|
||||
filepath=[u"does", u"not", u"exist"],
|
||||
|
151
mediagoblin/tests/test_notifications.py
Normal file
151
mediagoblin/tests/test_notifications.py
Normal file
@ -0,0 +1,151 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
|
||||
import urlparse
|
||||
|
||||
from mediagoblin.tools import template, mail
|
||||
|
||||
from mediagoblin.db.models import Notification, CommentNotification, \
|
||||
CommentSubscription
|
||||
from mediagoblin.db.base import Session
|
||||
|
||||
from mediagoblin.notifications import mark_comment_notification_seen
|
||||
|
||||
from mediagoblin.tests.tools import fixture_add_comment, \
|
||||
fixture_media_entry, fixture_add_user, \
|
||||
fixture_comment_subscription
|
||||
|
||||
|
||||
class TestNotifications:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, test_app):
|
||||
self.test_app = test_app
|
||||
|
||||
# TODO: Possibly abstract into a decorator like:
|
||||
# @as_authenticated_user('chris')
|
||||
self.test_user = fixture_add_user()
|
||||
|
||||
self.current_user = None
|
||||
|
||||
self.login()
|
||||
|
||||
def login(self, username=u'chris', password=u'toast'):
|
||||
response = self.test_app.post(
|
||||
'/auth/login/', {
|
||||
'username': username,
|
||||
'password': password})
|
||||
|
||||
response.follow()
|
||||
|
||||
assert urlparse.urlsplit(response.location)[2] == '/'
|
||||
assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
|
||||
|
||||
ctx = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
|
||||
|
||||
assert Session.merge(ctx['request'].user).username == username
|
||||
|
||||
self.current_user = ctx['request'].user
|
||||
|
||||
def logout(self):
|
||||
self.test_app.get('/auth/logout/')
|
||||
self.current_user = None
|
||||
|
||||
@pytest.mark.parametrize('wants_email', [True, False])
|
||||
def test_comment_notification(self, wants_email):
|
||||
'''
|
||||
Test
|
||||
- if a notification is created when posting a comment on
|
||||
another users media entry.
|
||||
- that the comment data is consistent and exists.
|
||||
|
||||
'''
|
||||
user = fixture_add_user('otherperson', password='nosreprehto',
|
||||
wants_comment_notification=wants_email)
|
||||
|
||||
user_id = user.id
|
||||
|
||||
media_entry = fixture_media_entry(uploader=user.id, state=u'processed')
|
||||
|
||||
media_entry_id = media_entry.id
|
||||
|
||||
subscription = fixture_comment_subscription(media_entry)
|
||||
|
||||
subscription_id = subscription.id
|
||||
|
||||
media_uri_id = '/u/{0}/m/{1}/'.format(user.username,
|
||||
media_entry.id)
|
||||
media_uri_slug = '/u/{0}/m/{1}/'.format(user.username,
|
||||
media_entry.slug)
|
||||
|
||||
self.test_app.post(
|
||||
media_uri_id + 'comment/add/',
|
||||
{
|
||||
'comment_content': u'Test comment #42'
|
||||
}
|
||||
)
|
||||
|
||||
notifications = Notification.query.filter_by(
|
||||
user_id=user.id).all()
|
||||
|
||||
assert len(notifications) == 1
|
||||
|
||||
notification = notifications[0]
|
||||
|
||||
assert type(notification) == CommentNotification
|
||||
assert notification.seen == False
|
||||
assert notification.user_id == user.id
|
||||
assert notification.subject.get_author.id == self.test_user.id
|
||||
assert notification.subject.content == u'Test comment #42'
|
||||
|
||||
if wants_email == True:
|
||||
assert mail.EMAIL_TEST_MBOX_INBOX == [
|
||||
{'from': 'notice@mediagoblin.example.org',
|
||||
'message': 'Content-Type: text/plain; \
|
||||
charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: \
|
||||
base64\nSubject: GNU MediaGoblin - chris commented on your \
|
||||
post\nFrom: notice@mediagoblin.example.org\nTo: \
|
||||
otherperson@example.com\n\nSGkgb3RoZXJwZXJzb24sCmNocmlzIGNvbW1lbnRlZCBvbiB5b3VyIHBvc3QgKGh0dHA6Ly9sb2Nh\nbGhvc3Q6ODAvdS9vdGhlcnBlcnNvbi9tL3NvbWUtdGl0bGUvYy8xLyNjb21tZW50KSBhdCBHTlUg\nTWVkaWFHb2JsaW4KClRlc3QgY29tbWVudCAjNDIKCkdOVSBNZWRpYUdvYmxpbg==\n',
|
||||
'to': [u'otherperson@example.com']}]
|
||||
else:
|
||||
assert mail.EMAIL_TEST_MBOX_INBOX == []
|
||||
|
||||
# Save the ids temporarily because of DetachedInstanceError
|
||||
notification_id = notification.id
|
||||
comment_id = notification.subject.id
|
||||
|
||||
self.logout()
|
||||
self.login('otherperson', 'nosreprehto')
|
||||
|
||||
self.test_app.get(media_uri_slug + '/c/{0}/'.format(comment_id))
|
||||
|
||||
notification = Notification.query.filter_by(id=notification_id).first()
|
||||
|
||||
assert notification.seen == True
|
||||
|
||||
self.test_app.get(media_uri_slug + '/notifications/silence/')
|
||||
|
||||
subscription = CommentSubscription.query.filter_by(id=subscription_id)\
|
||||
.first()
|
||||
|
||||
assert subscription.notify == False
|
||||
|
||||
notifications = Notification.query.filter_by(
|
||||
user_id=user_id).all()
|
||||
|
||||
# User should not have been notified
|
||||
assert len(notifications) == 1
|
@ -15,18 +15,17 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from paste.deploy import loadapp
|
||||
from webtest import TestApp
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.db.models import User, MediaEntry, Collection
|
||||
from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \
|
||||
CommentSubscription, CommentNotification
|
||||
from mediagoblin.tools import testing
|
||||
from mediagoblin.init.config import read_mediagoblin_config
|
||||
from mediagoblin.db.base import Session
|
||||
@ -171,7 +170,7 @@ def assert_db_meets_expected(db, expected):
|
||||
|
||||
|
||||
def fixture_add_user(username=u'chris', password=u'toast',
|
||||
active_user=True):
|
||||
active_user=True, wants_comment_notification=True):
|
||||
# Reuse existing user or create a new one
|
||||
test_user = User.query.filter_by(username=username).first()
|
||||
if test_user is None:
|
||||
@ -184,6 +183,8 @@ def fixture_add_user(username=u'chris', password=u'toast',
|
||||
test_user.email_verified = True
|
||||
test_user.status = u'active'
|
||||
|
||||
test_user.wants_comment_notification = wants_comment_notification
|
||||
|
||||
test_user.save()
|
||||
|
||||
# Reload
|
||||
@ -195,19 +196,71 @@ def fixture_add_user(username=u'chris', password=u'toast',
|
||||
return test_user
|
||||
|
||||
|
||||
def fixture_comment_subscription(entry, notify=True, send_email=None):
|
||||
if send_email is None:
|
||||
uploader = User.query.filter_by(id=entry.uploader).first()
|
||||
send_email = uploader.wants_comment_notification
|
||||
|
||||
cs = CommentSubscription(
|
||||
media_entry_id=entry.id,
|
||||
user_id=entry.uploader,
|
||||
notify=notify,
|
||||
send_email=send_email)
|
||||
|
||||
cs.save()
|
||||
|
||||
cs = CommentSubscription.query.filter_by(id=cs.id).first()
|
||||
|
||||
Session.expunge(cs)
|
||||
|
||||
return cs
|
||||
|
||||
|
||||
def fixture_add_comment_notification(entry_id, subject_id, user_id,
|
||||
seen=False):
|
||||
cn = CommentNotification(user_id=user_id,
|
||||
seen=seen,
|
||||
subject_id=subject_id)
|
||||
cn.save()
|
||||
|
||||
cn = CommentNotification.query.filter_by(id=cn.id).first()
|
||||
|
||||
Session.expunge(cn)
|
||||
|
||||
return cn
|
||||
|
||||
|
||||
def fixture_media_entry(title=u"Some title", slug=None,
|
||||
uploader=None, save=True, gen_slug=True):
|
||||
uploader=None, save=True, gen_slug=True,
|
||||
state=u'unprocessed', fake_upload=True,
|
||||
expunge=True):
|
||||
if uploader is None:
|
||||
uploader = fixture_add_user().id
|
||||
|
||||
entry = MediaEntry()
|
||||
entry.title = title
|
||||
entry.slug = slug
|
||||
entry.uploader = uploader or fixture_add_user().id
|
||||
entry.uploader = uploader
|
||||
entry.media_type = u'image'
|
||||
entry.state = state
|
||||
|
||||
if fake_upload:
|
||||
entry.media_files = {'thumb': ['a', 'b', 'c.jpg'],
|
||||
'medium': ['d', 'e', 'f.png'],
|
||||
'original': ['g', 'h', 'i.png']}
|
||||
entry.media_type = u'mediagoblin.media_types.image'
|
||||
|
||||
if gen_slug:
|
||||
entry.generate_slug()
|
||||
|
||||
if save:
|
||||
entry.save()
|
||||
|
||||
if expunge:
|
||||
entry = MediaEntry.query.filter_by(id=entry.id).first()
|
||||
|
||||
Session.expunge(entry)
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
@ -231,3 +284,25 @@ def fixture_add_collection(name=u"My first Collection", user=None):
|
||||
|
||||
return coll
|
||||
|
||||
def fixture_add_comment(author=None, media_entry=None, comment=None):
|
||||
if author is None:
|
||||
author = fixture_add_user().id
|
||||
|
||||
if media_entry is None:
|
||||
media_entry = fixture_media_entry().id
|
||||
|
||||
if comment is None:
|
||||
comment = \
|
||||
'Auto-generated test comment by user #{0} on media #{0}'.format(
|
||||
author, media_entry)
|
||||
|
||||
comment = MediaComment(author=author,
|
||||
media_entry=media_entry,
|
||||
content=comment)
|
||||
|
||||
comment.save()
|
||||
|
||||
Session.expunge(comment)
|
||||
|
||||
return comment
|
||||
|
||||
|
@ -90,7 +90,12 @@ def send_email(from_addr, to_addrs, subject, message_body):
|
||||
if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']:
|
||||
mhost = FakeMhost()
|
||||
elif not mg_globals.app_config['email_debug_mode']:
|
||||
mhost = smtplib.SMTP(
|
||||
if mg_globals.app_config['email_smtp_use_ssl']:
|
||||
smtp_init = smtplib.SMTP_SSL
|
||||
else:
|
||||
smtp_init = smtplib.SMTP
|
||||
|
||||
mhost = smtp_init(
|
||||
mg_globals.app_config['email_smtp_host'],
|
||||
mg_globals.app_config['email_smtp_port'])
|
||||
|
||||
|
@ -25,8 +25,9 @@ from mediagoblin.tools.response import render_to_response, render_404, \
|
||||
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||
from mediagoblin.tools.pagination import Pagination
|
||||
from mediagoblin.user_pages import forms as user_forms
|
||||
from mediagoblin.user_pages.lib import (send_comment_email,
|
||||
add_media_to_collection)
|
||||
from mediagoblin.user_pages.lib import add_media_to_collection
|
||||
from mediagoblin.notifications import trigger_notification, \
|
||||
add_comment_subscription, mark_comment_notification_seen
|
||||
|
||||
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
|
||||
get_media_entry_by_id,
|
||||
@ -34,6 +35,7 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
|
||||
get_user_collection, get_user_collection_item, active_user_from_url)
|
||||
|
||||
from werkzeug.contrib.atom import AtomFeed
|
||||
from werkzeug.exceptions import MethodNotAllowed
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@ -110,6 +112,7 @@ def user_gallery(request, page, url_user=None):
|
||||
'media_entries': media_entries,
|
||||
'pagination': pagination})
|
||||
|
||||
|
||||
MEDIA_COMMENTS_PER_PAGE = 50
|
||||
|
||||
|
||||
@ -121,6 +124,9 @@ def media_home(request, media, page, **kwargs):
|
||||
"""
|
||||
comment_id = request.matchdict.get('comment', None)
|
||||
if comment_id:
|
||||
if request.user:
|
||||
mark_comment_notification_seen(comment_id, request.user)
|
||||
|
||||
pagination = Pagination(
|
||||
page, media.get_comments(
|
||||
mg_globals.app_config['comments_ascending']),
|
||||
@ -154,7 +160,8 @@ def media_post_comment(request, media):
|
||||
"""
|
||||
recieves POST from a MediaEntry() comment form, saves the comment.
|
||||
"""
|
||||
assert request.method == 'POST'
|
||||
if not request.method == 'POST':
|
||||
raise MethodNotAllowed()
|
||||
|
||||
comment = request.db.MediaComment()
|
||||
comment.media_entry = media.id
|
||||
@ -179,11 +186,9 @@ def media_post_comment(request, media):
|
||||
request, messages.SUCCESS,
|
||||
_('Your comment has been posted!'))
|
||||
|
||||
media_uploader = media.get_uploader
|
||||
#don't send email if you comment on your own post
|
||||
if (comment.author != media_uploader and
|
||||
media_uploader.wants_comment_notification):
|
||||
send_comment_email(media_uploader, comment, media, request)
|
||||
trigger_notification(comment, media, request)
|
||||
|
||||
add_comment_subscription(request.user, media)
|
||||
|
||||
return redirect_obj(request, media)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user