Merge branch 'ticket-679' into OPW-Moderation-Update

Conflicts:
	mediagoblin/auth/tools.py
	mediagoblin/auth/views.py
	mediagoblin/db/migration_tools.py
	mediagoblin/db/migrations.py
	mediagoblin/db/models.py
	mediagoblin/decorators.py
	mediagoblin/user_pages/views.py
This commit is contained in:
tilly-Q 2013-07-29 18:40:19 -04:00
commit 52a355b275
140 changed files with 6692 additions and 6036 deletions

17
AUTHORS
View File

@ -8,10 +8,12 @@ variety of different ways and this software wouldn't exist without them.
Thank you!
* Aaron Williamson
* Aditi Mittal
* Aeva Ntsc
* Alejandro Villanueva
* Aleksandar Micovic
* Aleksej Serdjukov
* Alon Levy
* Alex Camelio
* András Veres-Szentkirályi
* Bassam Kurdali
@ -21,13 +23,17 @@ Thank you!
* Corey Farwell
* Chris Moylan
* Christopher Allan Webber
* David Thompson
* Daniel Neel
* Deb Nicholson
* Derek Moore
* Duncan Paterson
* Elrond of Samba TNG
* Emily O'Leary
* Gabi Thume
* Gabriel Saldana
* Greg Grossmeier
* Hans Lo
* Jakob Kramer
* Jef van Schendel
* Jessica Tallon
@ -36,25 +42,34 @@ Thank you!
* Jorge Araya Navarro
* Karen Rustad
* Kuno Woudt
* Laura Arjona
* Larisa Hoffenbecker
* Luke Slater
* Manuel Urbano Santos
* Mark Holmquist
* Mats Sjöberg
* Matt Lee
* Michele Azzolari
* Mike Linksvayer
* Natalie Foust-Pilcher
* Nathan Yergler
* Odin Hørthe Omdal
* Osama Khalid
* Pablo J. Urbano Santos
* Praveen Kumar
* Rasmus Larsson
* Rodney Ewing
* Runar Petursson
* Sacha De'Angeli
* Sam Kleinman
* Sam Tuke
* Sebastian Spaeth
* Shawn Khan
* Simon Fondrie-Teitler
* Stefano Zacchiroli
* Tiberiu C. Turbureanu
* Tran Thanh Bao
* Tryggvi Björgvinsson
* Shawn Khan
* Will Kahn-Greene
@ -64,4 +79,4 @@ If you think your name should be on this list, let us know!
We also are currently borrowing an image in
mediagoblin/static/images/media_thumbs/image.png from the wonderful
people at http://tango.freedesktop.org/ which is in the public
domain... thanks Tango folks!
domain... thanks Tango folks!

14
README
View File

@ -5,21 +5,21 @@
What is GNU MediaGoblin?
========================
* Initially, a place to store all your photos thats as awesome as, if
not more awesome than, existing network services (Flickr, SmugMug,
Picasa, etc)
* A place to store all your different media (photos, videos, audios,
and more!) thats as awesome as, if not more awesome than, existing
network services (Flickr, YouTube, etc)
* Customizable!
* A place for people to collaborate and show off original and derived
creations. Free, as in freedom. Were a GNU project after all.
* Later, a place for all sorts of media, such as video, music, etc hosting.
* Later, federated with OStatus!
* Extensible: Plugins allow you to add new media types (3d models?
Presentations and documents? Yes, and more!) or extend old ones.
* A real community, and we'd love to have you join us!
Is it ready for me to use?
==========================
Yes! But with caveats. The software is usable and there are instances
running, but it's still in its early stages.
Yes!
Can I help/hang out/participate/whisper sweet nothings in your ear?

View File

@ -0,0 +1,62 @@
.. MediaGoblin Documentation
Written in 2011, 2012 by MediaGoblin contributors
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to
the public domain worldwide. This software is distributed without
any warranty.
You should have received a copy of the CC0 Public Domain
Dedication along with this software. If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
==========
Migrations
==========
So, about migrations. Every time we change the way the database
structure works, we need to add a migration so that people running
older codebases can have their databases updated to the new structure
when they run `./bin/gmg dbupdate`.
The first time `./bin/gmg dbupdate` is run by a user, it creates the
tables at the current state that they're defined in models.py and sets
the migration number to the current migration... after all, migrations
only exist to get things to the current state of the db. After that,
every migration is run with dbupdate.
There's a few things you need to know:
- We use `sqlalchemy-migrate
<http://code.google.com/p/sqlalchemy-migrate/>`_.
See `their docs <https://sqlalchemy-migrate.readthedocs.org/>`_.
- `Alembic <https://bitbucket.org/zzzeek/alembic>`_ might be a better
choice than sqlalchemy-migrate now or in the future, but we
originally decided not to use it because it didn't have sqlite
support. It's not clear if that's changed.
- SQLAlchemy has two parts to it, the ORM and the "core" interface.
We DO NOT use the ORM when running migrations. Think about it: the
ORM is set up with an expectation that the models already reflect a
certain pattern. But if a person is moving from their old patern
and are running tools to *get to* the current pattern, of course
their current database structure doesn't match the state of the ORM!
- How to write migrations? Maybe there will be a tutorial here in the
future... in the meanwhile, look at existing migrations in
`mediagoblin/db/migrations.py` and look in
`mediagoblin/tests/test_sql_migrations.py` for examples.
- Common pattern: use `inspect_table` to get the current state
of the table before we run alterations on it.
- Make sure you set the RegisterMigration to be the next migration in
order.
- What happens if you're adding a *totally new* table? In this case,
you should copy the table in entirety as it exists into
migrations.py then create the tables based off of that... see
add_collection_tables. This is easier than reproducing the SQL by
hand.
- If you're writing a feature branch, you don't need to keep adding
migrations every time you change things around if your database
structure is in flux. Just alter your migrations so that they're
correct for the merge into master.
That's it for now! Good luck!

View File

@ -74,6 +74,7 @@ This guide covers writing new GNU MediaGoblin plugins.
pluginwriter/database
pluginwriter/api
pluginwriter/tests
pluginwriter/media_type_hooks
Part 4: Developer's Zone
@ -87,6 +88,7 @@ This chapter contains various information for developers.
devel/codebase
devel/storage
devel/originaldesigndecisions
devel/migrations
Indices and tables

View File

@ -69,6 +69,32 @@ example might look like::
This means that when people enable your plugin in their config you'll
be able to provide defaults as well as type validation.
You can access this via the app_config variables in mg_globals, or you
can use a shortcut to get your plugin's config section::
>>> from mediagoblin.tools import pluginapi
# Replace with the path to your plugin.
# (If an external package, it won't be part of mediagoblin.plugins)
>>> floobie_config = pluginapi.get_config('mediagoblin.plugins.floobifier')
>>> floobie_dir = floobie_config['floobie_dir']
# This is the same as the above
>>> from mediagoblin import mg_globals
>>> config = mg_globals.global_config['plugins']['mediagoblin.plugins.floobifier']
>>> floobie_dir = floobie_config['floobie_dir']
A tip: you have access to the `%(here)s` variable in your config,
which is the directory that the user's mediagoblin config is running
out of. So for example, your plugin may need a "floobie" directory to
store floobs in. You could give them a reasonable default that makes
use of the default `user_dev` location, but allow users to override
it, like so::
[plugin_spec]
floobie_dir = string(default="%(here)s/user_dev/floobs/")
Note, this is relative to the user's mediagoblin config directory,
*not* your plugin directory!
Context Hooks
-------------

View File

@ -0,0 +1,38 @@
==================
Media Type hooks
==================
This documents the hooks that are currently available for ``media_type`` plugins.
What hooks are available?
=========================
'sniff_handler'
---------------
This hook is used by ``sniff_media`` in ``mediagoblin.media_types.__init__``.
Your media type should return its ``sniff_media`` method when this hook is
called.
.. Note::
Your ``sniff_media`` method should return either the ``media_type`` or
``None``.
'get_media_type_and_manager'
----------------------------
This hook is used by ``get_media_type_and_manager`` in
``mediagoblin.media_types.__init__``. When this hook is called, your media type
plugin should check if it can handle the given extension. If so, your media
type plugin should return the media type and media manager.
('media_manager', MEDIA_TYPE)
-----------------------------
If you already know the string representing the media type of a type
of media, you can pull down the manager specifically. Note that this
hook is not a string but a tuple of two strings, the latter being the
name of the media type.
This is used by media entries to pull down their media managers, and
so on.

View File

@ -157,13 +157,14 @@ directory. Modify these commands to reflect your own environment::
mkdir -p /srv/mediagoblin.example.org/
cd /srv/mediagoblin.example.org/
Clone the MediaGoblin repository::
Clone the MediaGoblin repository and set up the git submodules::
git clone git://gitorious.org/mediagoblin/mediagoblin.git
cd mediagoblin
git submodule init && git submodule update
And set up the in-package virtualenv::
cd mediagoblin
(virtualenv --system-site-packages . || virtualenv .) && ./bin/python setup.py develop
.. note::
@ -194,7 +195,7 @@ This concludes the initial configuration of the development
environment. In the future, when you update your
codebase, you should also run::
./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate
./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate && git submodule fetch
Note: If you are running an active site, depending on your server
configuration, you may need to stop it first or the dbupdate command

View File

@ -18,16 +18,18 @@ Media Types
====================
In the future, there will be all sorts of media types you can enable,
but in the meanwhile there are three additional media types: video, audio
and ascii art.
but in the meanwhile there are five additional media types: video, audio,
ascii art, STL/3d models, PDF and Document.
First, you should probably read ":doc:`configuration`" to make sure
you know how to modify the mediagoblin config file.
Enabling Media Types
====================
.. note::
Media types are now plugins
Media types are enabled in your mediagoblin configuration file, typically it is
created by copying ``mediagoblin.ini`` to ``mediagoblin_local.ini`` and then
applying your changes to ``mediagoblin_local.ini``. If you don't already have a
@ -37,11 +39,13 @@ Most media types have additional dependencies that you will have to install.
You will find descriptions on how to satisfy the requirements of each media type
on this page.
To enable a media type, edit the ``media_types`` list in your
``mediagoblin_local.ini``. For example, if your system supported image and
video media types, then the list would look like this::
To enable a media type, add the the media type under the ``[plugins]`` section
in you ``mediagoblin_local.ini``. For example, if your system supported image
and video media types, then it would look like this::
media_types = mediagoblin.media_types.image, mediagoblin.media_types.video
[plugins]
[[mediagoblin.media_types.image]]
[[mediagoblin.media_types.video]]
Note that after enabling new media types, you must run dbupdate like so::
@ -83,8 +87,8 @@ good/bad/ugly). On Debianoid systems
gstreamer0.10-ffmpeg
Add ``mediagoblin.media_types.video`` to the ``media_types`` list in your
``mediagoblin_local.ini`` and restart MediaGoblin.
Add ``[[mediagoblin.media_types.video]]`` under the ``[plugins]`` section in
your ``mediagoblin_local.ini`` and restart MediaGoblin.
Run
@ -133,7 +137,7 @@ Then install ``scikits.audiolab`` for the spectrograms::
./bin/pip install scikits.audiolab
Add ``mediagoblin.media_types.audio`` to the ``media_types`` list in your
Add ``[[mediagoblin.media_types.audio]]`` under the ``[plugins]`` section in your
``mediagoblin_local.ini`` and restart MediaGoblin.
Run
@ -158,13 +162,8 @@ library, which is necessary for creating thumbnails of ascii art
Next, modify (and possibly copy over from ``mediagoblin.ini``) your
``mediagoblin_local.ini``. In the ``[mediagoblin]`` section, add
``mediagoblin.media_types.ascii`` to the ``media_types`` list.
For example, if your system supported image and ascii art media types, then
the list would look like this::
media_types = mediagoblin.media_types.image, mediagoblin.media_types.ascii
``mediagoblin_local.ini``. In the ``[plugins]`` section, add
``[[mediagoblin.media_types.ascii]]``.
Run
@ -184,7 +183,7 @@ your execution path. This feature has been tested with Blender 2.63.
It may work on some earlier versions, but that is not guaranteed (and
is surely not to work prior to Blender 2.5X).
Add ``mediagoblin.media_types.stl`` to the ``media_types`` list in your
Add ``[[mediagoblin.media_types.stl]]`` under the ``[plugins]`` section in your
``mediagoblin_local.ini`` and restart MediaGoblin.
Run
@ -199,8 +198,16 @@ will be able to present them to your wide audience of admirers!
PDF and Document
================
To enable the "PDF and Document" support plugin, you need pdftocairo, pdfinfo,
unoconv with headless support. All executables must be on your execution path.
To enable the "PDF and Document" support plugin, you need:
1. pdftocairo and pdfinfo for pdf only support.
2. unoconv with headless support to support converting libreoffice supported
documents as well, such as doc/ppt/xls/odf/odg/odp and more.
For the full list see mediagoblin/media_types/pdf/processing.py,
unoconv_supported.
All executables must be on your execution path.
To install this on Fedora:
@ -208,6 +215,9 @@ To install this on Fedora:
sudo yum install -y poppler-utils unoconv libreoffice-headless
Note: You can leave out unoconv and libreoffice-headless if you want only pdf
support. This will result in a much smaller list of dependencies.
pdf.js relies on git submodules, so be sure you have fetched them:
.. code-block:: bash
@ -222,7 +232,7 @@ This feature has been tested on Fedora with:
It may work on some earlier versions, but that is not guaranteed.
Add ``mediagoblin.media_types.pdf`` to the ``media_types`` list in your
Add ``[[mediagoblin.media_types.pdf]]`` under the ``[plugins]`` section in your
``mediagoblin_local.ini`` and restart MediaGoblin.
Run

View File

@ -21,11 +21,28 @@ This chapter has important information for releases in it.
If you're upgrading from a previous release, please read it
carefully, or at least skim over it.
0.4.1
=====
This is a bugfix release for 0.4.0. This only implements one major
fix in the newly released document support which prevented the
"conversion via libreoffice" feature.
If you were running 0.4.0 you can upgrade to v0.4.1 via a simple
switch and restarting mediagoblin/celery with no other actions.
Otherwise, follow 0.4.0 instructions.
0.4.0
=====
**Do this to upgrade**
1. Make sure to run ``bin/gmg dbupdate`` after upgrading.
1. Make sure to run
``./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate``
after upgrading.
2. See "For Theme authors" if you have a custom theme.
3. Note that ``./bin/gmg theme assetlink`` is now just
``./bin/gmg assetlink`` and covers both plugins and assets.
@ -39,6 +56,16 @@ carefully, or at least skim over it.
alias /srv/mediagoblin.example.org/mediagoblin/user_dev/plugin_static/;
}
Similarly, if you've got a modified paste config, you may want to
borrow the app:plugin_static section from the default paste.ini
file.
5. We now use itsdangerous for sessions; if you had any references to
beaker in your paste config you can remove them. Again, see the
default paste.ini config
6. We also now use git submodules. Please do:
``git submodule init && git submodule update``
You will need to do this to use the new PDF support.
**For theme authors**
If you have your own theme or you have any "user modified templates",
@ -51,7 +78,23 @@ please note the following:
You can easily customize this to give a welcome page appropriate to
your site.
**New features**
* PDF media type!
* Improved plugin system. More flexible, better documented, with a
new plugin authoring section of the docs.
* itsdangerous based sessions. No more beaker!
* New, experimental Piwigo-based API. This means you should be able
to use MediaGoblin with something like Shotwell. (Again, a word of
caution: this is *very experimental*!)
* Human readable timestamps, and the option to display the original
date of an image when available (available as the
"original_date_visible" variable)
* Moved unit testing system from nosetests to py.test so we can better
handle issues with sqlalchemy exploding with different database
configurations. Long story :)
* You can now disable the ability to post comments.
* Tags now can be up to length 255 characters by default.
0.3.3

View File

@ -11,19 +11,15 @@ email_sender_address = "notice@mediagoblin.example.org"
## Uncomment and change to your DB's appropiate setting.
## Default is a local sqlite db "mediagoblin.db".
## Don't forget to run `./bin/gmg dbupdate` after having changed it.
# sql_engine = postgresql:///mediagoblin
# set to false to enable sending notices
# Set to false to enable sending notices
email_debug_mode = true
# Set to false to disable registrations
allow_registration = true
## Uncomment this to turn on video or enable other media types
## You may have to install dependencies, and will have to run ./bin/gmg dbupdate
## See http://docs.mediagoblin.org/siteadmin/media-types.html for details.
# media_types = mediagoblin.media_types.image, mediagoblin.media_types.video
## Uncomment this to put some user-overriding templates here
# local_templates = %(here)s/user_dev/templates/
@ -43,7 +39,9 @@ base_url = /mgoblin_media/
[celery]
# Put celery stuff here
# place plugins here---each in their own subsection of [plugins]. see
# documentation for details.
# Place plugins here, each in their own subsection of [plugins].
# See http://docs.mediagoblin.org/siteadmin/plugins.html for details.
[plugins]
[[mediagoblin.plugins.geolocation]]
[[mediagoblin.plugins.basic_auth]]
[[mediagoblin.media_types.image]]

View File

@ -23,4 +23,4 @@
# see http://www.python.org/dev/peps/pep-0386/
__version__ = "0.4.0.dev"
__version__ = "0.5.0.dev"

View File

@ -29,6 +29,7 @@ from mediagoblin.tools import common, session, translate, template
from mediagoblin.tools.response import render_http_exception
from mediagoblin.tools.theme import register_themes
from mediagoblin.tools import request as mg_request
from mediagoblin.media_types.tools import media_type_warning
from mediagoblin.mg_globals import setup_globals
from mediagoblin.init.celery import setup_celery_from_config
from mediagoblin.init.plugins import setup_plugins
@ -37,6 +38,8 @@ 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.auth.tools import check_auth_enabled, no_auth_logout
from mediagoblin import notifications
_log = logging.getLogger(__name__)
@ -67,6 +70,8 @@ class MediaGoblinApp(object):
# Open and setup the config
global_config, app_config = setup_global_and_app_config(config_path)
media_type_warning()
setup_crypto()
##########################################
@ -85,7 +90,7 @@ class MediaGoblinApp(object):
setup_plugins()
# Set up the database
self.db = setup_database()
self.db = setup_database(app_config['run_migrations'])
# Register themes
self.theme_registry, self.current_theme = register_themes(app_config)
@ -97,6 +102,11 @@ class MediaGoblinApp(object):
PluginManager().get_template_paths()
)
# Check if authentication plugin is enabled and respond accordingly.
self.auth = check_auth_enabled()
if not self.auth:
app_config['allow_comments'] = False
# Set up storage systems
self.public_store, self.queue_store = setup_storage()
@ -186,6 +196,11 @@ class MediaGoblinApp(object):
request.urlgen = build_proxy
# Log user out if authentication_disabled
no_auth_logout(request)
request.notifications = notifications
mg_request.setup_user_in_request(request)
request.controller_name = None

View File

@ -13,3 +13,32 @@
#
# 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.pluginapi import hook_handle, hook_runall
def get_user(**kwargs):
""" Takes a kwarg such as username and returns a user object """
return hook_handle("auth_get_user", **kwargs)
def create_user(register_form):
results = hook_runall("auth_create_user", register_form)
return results[0]
def extra_validation(register_form):
from mediagoblin.auth.tools import basic_extra_validation
extra_validation_passes = basic_extra_validation(register_form)
if False in hook_runall("auth_extra_validation", register_form):
extra_validation_passes = False
return extra_validation_passes
def gen_password_hash(raw_pass, extra_salt=None):
return hook_handle("auth_gen_password_hash", raw_pass, extra_salt)
def check_password(raw_pass, stored_hash, extra_salt=None):
return hook_handle("auth_check_password",
raw_pass, stored_hash, extra_salt)

View File

@ -20,32 +20,6 @@ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.auth.tools import normalize_user_or_email_field
class RegistrationForm(wtforms.Form):
username = wtforms.TextField(
_('Username'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_email=False)])
password = wtforms.PasswordField(
_('Password'),
[wtforms.validators.Required(),
wtforms.validators.Length(min=5, max=1024)])
email = wtforms.TextField(
_('Email address'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_user=False)])
class LoginForm(wtforms.Form):
username = wtforms.TextField(
_('Username or Email'),
[wtforms.validators.Required(),
normalize_user_or_email_field()])
password = wtforms.PasswordField(
_('Password'),
[wtforms.validators.Required(),
wtforms.validators.Length(min=5, max=1024)])
class ForgotPassForm(wtforms.Form):
username = wtforms.TextField(
_('Username or email'),
@ -58,9 +32,6 @@ class ChangePassForm(wtforms.Form):
'Password',
[wtforms.validators.Required(),
wtforms.validators.Length(min=5, max=1024)])
userid = wtforms.HiddenField(
'',
[wtforms.validators.Required()])
token = wtforms.HiddenField(
'',
[wtforms.validators.Required()])

View File

@ -14,19 +14,20 @@
# 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 uuid
import logging
import logging
import wtforms
from sqlalchemy import or_
from mediagoblin import mg_globals
from mediagoblin.auth import lib as auth_lib
from mediagoblin.db.models import User, Privilege
from mediagoblin.tools.crypto import get_timed_signer_url
from mediagoblin.db.models import User
from mediagoblin.tools.mail import (normalize_email, send_email,
email_debug_message)
from mediagoblin.tools.template import render_template
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin import auth
_log = logging.getLogger(__name__)
@ -62,11 +63,12 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True):
EMAIL_VERIFICATION_TEMPLATE = (
u"http://{host}{uri}?"
u"userid={userid}&token={verification_key}")
u"{uri}?"
u"token={verification_key}")
def send_verification_email(user, request):
def send_verification_email(user, request, email=None,
rendered_email=None):
"""
Send the verification email to users to activate their accounts.
@ -74,19 +76,24 @@ def send_verification_email(user, request):
- user: a user object
- request: the request
"""
rendered_email = render_template(
request, 'mediagoblin/auth/verification_email.txt',
{'username': user.username,
'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
host=request.host,
uri=request.urlgen('mediagoblin.auth.verify_email'),
userid=unicode(user.id),
verification_key=user.verification_key)})
if not email:
email = user.email
if not rendered_email:
verification_key = get_timed_signer_url('mail_verification_token') \
.dumps(user.id)
rendered_email = render_template(
request, 'mediagoblin/auth/verification_email.txt',
{'username': user.username,
'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
uri=request.urlgen('mediagoblin.auth.verify_email',
qualified=True),
verification_key=verification_key)})
# TODO: There is no error handling in place
send_email(
mg_globals.app_config['email_sender_address'],
[user.email],
[email],
# TODO
# Due to the distributed nature of GNU MediaGoblin, we should
# find a way to send some additional information about the
@ -96,11 +103,43 @@ def send_verification_email(user, request):
rendered_email)
EMAIL_FP_VERIFICATION_TEMPLATE = (
u"{uri}?"
u"token={fp_verification_key}")
def send_fp_verification_email(user, request):
"""
Send the verification email to users to change their password.
Args:
- user: a user object
- request: the request
"""
fp_verification_key = get_timed_signer_url('mail_verification_token') \
.dumps(user.id)
rendered_email = render_template(
request, 'mediagoblin/auth/fp_verification_email.txt',
{'username': user.username,
'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format(
uri=request.urlgen('mediagoblin.auth.verify_forgot_password',
qualified=True),
fp_verification_key=fp_verification_key)})
# TODO: There is no error handling in place
send_email(
mg_globals.app_config['email_sender_address'],
[user.email],
'GNU MediaGoblin - Change forgotten password!',
rendered_email)
def basic_extra_validation(register_form, *args):
users_with_username = User.query.filter_by(
username=register_form.data['username']).count()
username=register_form.username.data).count()
users_with_email = User.query.filter_by(
email=register_form.data['email']).count()
email=register_form.email.data).count()
extra_validation_passes = True
@ -118,17 +157,11 @@ def basic_extra_validation(register_form, *args):
def register_user(request, register_form):
""" Handle user registration """
extra_validation_passes = basic_extra_validation(register_form)
extra_validation_passes = auth.extra_validation(register_form)
if extra_validation_passes:
# Create the user
user = User()
user.username = register_form.data['username']
user.email = register_form.data['email']
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
register_form.password.data)
user.verification_key = unicode(uuid.uuid4())
user.save()
user = auth.create_user(register_form)
# give the user the default privileges
default_privileges = [
@ -151,17 +184,37 @@ def register_user(request, register_form):
return None
def check_login_simple(username, password, username_might_be_email=False):
search = (User.username == username)
if username_might_be_email and ('@' in username):
search = or_(search, User.email == username)
user = User.query.filter(search).first()
def check_login_simple(username, password):
user = auth.get_user(username=username)
if not user:
_log.info("User %r not found", username)
auth_lib.fake_login_attempt()
hook_handle("auth_fake_login_attempt")
return None
if not auth_lib.bcrypt_check_password(password, user.pw_hash):
if not auth.check_password(password, user.pw_hash):
_log.warn("Wrong password for %r", username)
return None
_log.info("Logging %r in", username)
return user
def check_auth_enabled():
if not hook_handle('authentication'):
_log.warning('No authentication is enabled')
return False
else:
return True
def no_auth_logout(request):
"""Log out the user if authentication_disabled, but don't delete the messages"""
if not mg_globals.app.auth and 'user_id' in request.session:
del request.session['user_id']
request.session.save()
def create_basic_user(form):
user = User()
user.username = form.username.data
user.email = form.email.data
user.save()
return user

View File

@ -14,36 +14,37 @@
# 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 uuid
import datetime
from itsdangerous import BadSignature
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import User, Privilege
from mediagoblin.tools.crypto import get_timed_signer_url
from mediagoblin.decorators import auth_enabled, allow_registration
from mediagoblin.tools.response import render_to_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.mail import email_debug_message
from mediagoblin.auth import lib as auth_lib
from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.auth import forms as auth_forms
from mediagoblin.auth.lib import send_fp_verification_email
from mediagoblin.auth.tools import (send_verification_email, register_user,
send_fp_verification_email,
check_login_simple)
from mediagoblin import auth
@allow_registration
@auth_enabled
def register(request):
"""The registration view.
Note that usernames will always be lowercased. Email domains are lowercased while
the first part remains case-sensitive.
"""
# Redirects to indexpage if registrations are disabled
if not mg_globals.app_config["allow_registration"]:
messages.add_message(
request,
messages.WARNING,
_('Sorry, registration is disabled on this instance.'))
return redirect(request, "index")
if 'pass_auth' not in request.template_env.globals:
redirect_name = hook_handle('auth_no_pass_redirect')
return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
redirect_name))
register_form = auth_forms.RegistrationForm(request.form)
register_form = hook_handle("auth_get_registration_form", request)
if request.method == 'POST' and register_form.validate():
# TODO: Make sure the user doesn't exist already
@ -59,28 +60,36 @@ def register(request):
return render_to_response(
request,
'mediagoblin/auth/register.html',
{'register_form': register_form})
{'register_form': register_form,
'post_url': request.urlgen('mediagoblin.auth.register')})
@auth_enabled
def login(request):
"""
MediaGoblin login view.
If you provide the POST with 'next', it'll redirect to that view.
"""
login_form = auth_forms.LoginForm(request.form)
if 'pass_auth' not in request.template_env.globals:
redirect_name = hook_handle('auth_no_pass_redirect')
return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
redirect_name))
login_form = hook_handle("auth_get_login_form", request)
login_failed = False
if request.method == 'POST':
username = login_form.data['username']
username = login_form.username.data
if login_form.validate():
user = check_login_simple(username, login_form.password.data, True)
user = check_login_simple(username, login_form.password.data)
if user:
# set up login in session
if login_form.stay_logged_in.data:
request.session['stay_logged_in'] = True
request.session['user_id'] = unicode(user.id)
request.session.save()
@ -97,6 +106,7 @@ def login(request):
{'login_form': login_form,
'next': request.GET.get('next') or request.form.get('next'),
'login_failed': login_failed,
'post_url': request.urlgen('mediagoblin.auth.login'),
'allow_registration': mg_globals.app_config["allow_registration"]})
@ -115,12 +125,26 @@ def verify_email(request):
you are lucky :)
"""
# If we don't have userid and token parameters, we can't do anything; 404
if not 'userid' in request.GET or not 'token' in request.GET:
if not 'token' in request.GET:
return render_404(request)
user = User.query.filter_by(id=request.args['userid']).first()
# Catch error if token is faked or expired
try:
token = get_timed_signer_url("mail_verification_token") \
.loads(request.GET['token'], max_age=10*24*3600)
except BadSignature:
messages.add_message(
request,
messages.ERROR,
_('The verification key or user id is incorrect.'))
if user and user.verification_key == unicode(request.GET['token']):
return redirect(
request,
'index')
user = User.query.filter_by(id=int(token)).first()
if user and user.email_verified is False:
user.status = u'active'
user.email_verified = True
user.verification_key = None
@ -169,9 +193,6 @@ def resend_activation(request):
return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
request.user.verification_key = unicode(uuid.uuid4())
request.user.save()
email_debug_message(request)
send_verification_email(request.user, request)
@ -191,13 +212,16 @@ def forgot_password(request):
Sends an email with an url to renew forgotten password.
Use GET querystring parameter 'username' to pre-populate the input field
"""
if not 'pass_auth' in request.template_env.globals:
return redirect(request, 'index')
fp_form = auth_forms.ForgotPassForm(request.form,
username=request.args.get('username'))
if not (request.method == 'POST' and fp_form.validate()):
# Either GET request, or invalid form submitted. Display the template
return render_to_response(request,
'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form})
'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form,})
# If we are here: method == POST and form is valid. username casing
# has been sanitized. Store if a user was found by email. We should
@ -238,11 +262,6 @@ def forgot_password(request):
# SUCCESS. Send reminder and return to login page
if user:
user.fp_verification_key = unicode(uuid.uuid4())
user.fp_token_expire = datetime.datetime.now() + \
datetime.timedelta(days=10)
user.save()
email_debug_message(request)
send_fp_verification_email(user, request)
@ -257,31 +276,44 @@ def verify_forgot_password(request):
"""
# get form data variables, and specifically check for presence of token
formdata = _process_for_token(request)
if not formdata['has_userid_and_token']:
if not formdata['has_token']:
return render_404(request)
formdata_token = formdata['vars']['token']
formdata_userid = formdata['vars']['userid']
formdata_vars = formdata['vars']
# check if it's a valid user id
user = User.query.filter_by(id=formdata_userid).first()
if not user:
return render_404(request)
# Catch error if token is faked or expired
try:
token = get_timed_signer_url("mail_verification_token") \
.loads(formdata_vars['token'], max_age=10*24*3600)
except BadSignature:
messages.add_message(
request,
messages.ERROR,
_('The verification key or user id is incorrect.'))
# check if we have a real user and correct token
if ((user and user.fp_verification_key and
user.fp_verification_key == unicode(formdata_token) and
datetime.datetime.now() < user.fp_token_expire
and user.email_verified and user.status == 'active')):
return redirect(
request,
'index')
# check if it's a valid user id
user = User.query.filter_by(id=int(token)).first()
# no user in db
if not user:
messages.add_message(
request, messages.ERROR,
_('The user id is incorrect.'))
return redirect(
request, 'index')
# check if user active and has email verified
if user.email_verified and user.status == 'active':
cp_form = auth_forms.ChangePassForm(formdata_vars)
if request.method == 'POST' and cp_form.validate():
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
user.pw_hash = auth.gen_password_hash(
cp_form.password.data)
user.fp_verification_key = None
user.fp_token_expire = None
user.save()
messages.add_message(
@ -293,12 +325,22 @@ def verify_forgot_password(request):
return render_to_response(
request,
'mediagoblin/auth/change_fp.html',
{'cp_form': cp_form})
{'cp_form': cp_form,})
# in case there is a valid id but no user with that id in the db
# or the token expired
else:
return render_404(request)
if not user.email_verified:
messages.add_message(
request, messages.ERROR,
_('You need to verify your email before you can reset your'
' password.'))
if not user.status == 'active':
messages.add_message(
request, messages.ERROR,
_('You are no longer an active user. Please contact the system'
' admin to reactivate your accoutn.'))
return redirect(
request, 'index')
def _process_for_token(request):
@ -316,7 +358,6 @@ def _process_for_token(request):
formdata = {
'vars': formdata_vars,
'has_userid_and_token':
'userid' in formdata_vars and 'token' in formdata_vars}
'has_token': 'token' in formdata_vars}
return formdata

View File

@ -5,12 +5,13 @@ html_title = string(default="GNU MediaGoblin")
# link to source for this MediaGoblin site
source_link = string(default="https://gitorious.org/mediagoblin/mediagoblin")
# Enabled media types
media_types = string_list(default=list("mediagoblin.media_types.image"))
# database stuff
sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db")
# This flag is used during testing to allow use of in-memory SQLite
# databases. It is not recommended to be used on a running instance.
run_migrations = boolean(default=False)
# Where temporary files used in processing and etc are kept
workbench_path = string(default="%(here)s/user_dev/media/workbench")
@ -22,9 +23,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)

View File

@ -24,18 +24,6 @@ Session = scoped_session(sessionmaker())
class GMGTableBase(object):
query = Session.query_property()
@classmethod
def find(cls, query_dict):
return cls.query.filter_by(**query_dict)
@classmethod
def find_one(cls, query_dict):
return cls.query.filter_by(**query_dict).first()
@classmethod
def one(cls, query_dict):
return cls.find(query_dict).one()
def get(self, key):
return getattr(self, key)

View File

@ -29,7 +29,7 @@ class MigrationManager(object):
to the latest migrations, etc.
"""
def __init__(self, name, models, migration_registry, session,
def __init__(self, name, models, foundations, migration_registry, session,
printer=simple_printer):
"""
Args:
@ -40,6 +40,7 @@ class MigrationManager(object):
"""
self.name = unicode(name)
self.models = models
self.foundations = foundations
self.session = session
self.migration_registry = migration_registry
self._sorted_migrations = None
@ -145,11 +146,11 @@ class MigrationManager(object):
Create the table foundations (default rows) as layed out in FOUNDATIONS
in mediagoblin.db.models
"""
from mediagoblin.db.models import FOUNDATIONS as MAIN_FOUNDATIONS
for Model in MAIN_FOUNDATIONS.keys():
for parameters in MAIN_FOUNDATIONS[Model]:
row = Model(*parameters)
row.save()
for Model, rows in self.foundations.items():
print u'\n + Laying foundations for %s table' % (Model.__name__)
for parameters in rows:
new_row = Model(**parameters)
new_row.save()
def create_new_migration_record(self):
"""
@ -186,8 +187,7 @@ class MigrationManager(object):
if self.name == u'__main__':
return u"main mediagoblin tables"
else:
# TODO: Use the friendlier media manager "human readable" name
return u'media type "%s"' % self.name
return u'plugin "%s"' % self.name
def init_or_migrate(self):
"""
@ -214,10 +214,9 @@ class MigrationManager(object):
self.init_tables()
# auto-set at latest migration number
self.create_new_migration_record()
if self.name==u'__main__':
self.populate_table_foundations()
self.create_new_migration_record()
self.populate_table_foundations()
self.printer(u"done.\n")
self.set_current_migration()
return u'inited'

View File

@ -289,10 +289,99 @@ def unique_collections_slug(db):
db.commit()
class ReportBase_v0(declarative_base()):
@RegisterMigration(11, MIGRATIONS)
def drop_token_related_User_columns(db):
"""
Drop unneeded columns from the User table after switching to using
itsdangerous tokens for email and forgot password verification.
"""
metadata = MetaData(bind=db.bind)
user_table = inspect_table(metadata, 'core__users')
"""
verification_key = user_table.columns['verification_key']
fp_verification_key = user_table.columns['fp_verification_key']
fp_token_expire = user_table.columns['fp_token_expire']
verification_key.drop()
fp_verification_key.drop()
fp_token_expire.drop()
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(12, 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)
@RegisterMigration(13, MIGRATIONS)
def pw_hash_nullable(db):
"""Make pw_hash column nullable"""
metadata = MetaData(bind=db.bind)
user_table = inspect_table(metadata, "core__users")
user_table.c.pw_hash.alter(nullable=True)
# sqlite+sqlalchemy seems to drop this constraint during the
# migration, so we add it back here for now a bit manually.
if db.bind.url.drivername == 'sqlite':
constraint = UniqueConstraint('username', table=user_table)
constraint.create()
class ReportBase_v0(declarative_base()):
__tablename__ = 'core__reports'
id = Column(Integer, primary_key=True)
reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
@ -302,7 +391,6 @@ class ReportBase_v0(declarative_base()):
discriminator = Column('type', Unicode(50))
__mapper_args__ = {'polymorphic_on': discriminator}
class CommentReport_v0(ReportBase_v0):
__tablename__ = 'core__reports_on_comments'
__mapper_args__ = {'polymorphic_identity': 'comment_report'}
@ -316,14 +404,13 @@ class MediaReport_v0(ReportBase_v0):
__mapper_args__ = {'polymorphic_identity': 'media_report'}
id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True)
media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
media_entry_id = Column(Integer, ForeignKey(MediaEntry.i
class ArchivedReport_v0(ReportBase_v0):
__tablename__ = 'core__reports_archived'
__mapper_args__ = {'polymorphic_identity': 'archived_report'}
id = Column('id',Integer, ForeignKey('core__reports.id'))
media_entry_id = Column(Integer, ForeignKey(MediaEntry.id))
comment_id = Column(Integer, ForeignKey(MediaComment.id))
resolver_id = Column(Integer, ForeignKey(User.id), nullable=False)
@ -344,7 +431,6 @@ class Privilege_v0(declarative_base()):
class PrivilegeUserAssociation_v0(declarative_base()):
__tablename__ = 'core__privileges_users'
group_id = Column(
'core__privilege_id',
Integer,
@ -356,7 +442,7 @@ class PrivilegeUserAssociation_v0(declarative_base()):
ForeignKey(Privilege.id),
primary_key=True)
@RegisterMigration(11, MIGRATIONS)
@RegisterMigration(14, MIGRATIONS)
def create_moderation_tables(db):
ReportBase_v0.__table__.create(db.bind)
CommentReport_v0.__table__.create(db.bind)

View File

@ -29,13 +29,14 @@ real objects.
import uuid
import re
import datetime
from datetime import datetime
from werkzeug.utils import cached_property
from mediagoblin import mg_globals
from mediagoblin.media_types import get_media_managers, FileTypeNotSupported
from mediagoblin.media_types import FileTypeNotSupported
from mediagoblin.tools import common, licenses
from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.url import slugify
@ -201,14 +202,14 @@ class MediaEntryMixin(GenerateSlugMixin):
Raises FileTypeNotSupported in case no such manager is enabled
"""
# TODO, we should be able to make this a simple lookup rather
# than iterating through all media managers.
for media_type, manager in get_media_managers():
if media_type == self.media_type:
return manager(self)
manager = hook_handle(('media_manager', self.media_type))
if manager:
return manager(self)
# Not found? Then raise an error
raise FileTypeNotSupported(
"MediaManager not in enabled types. Check media_types in config?")
"MediaManager not in enabled types. Check media_type plugins are"
" enabled in config?")
def get_fail_exception(self):
"""
@ -287,6 +288,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):

View File

@ -24,16 +24,16 @@ 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 sqlalchemy.schema import Table
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
@ -239,8 +239,8 @@ class MediaEntry(Base, MediaEntryMixin):
This will *not* automatically delete unused collections, which
can remain empty...
:keyword del_orphan_tags: True/false if we delete unused Tags too
:keyword commit: True/False if this should end the db transaction"""
:param del_orphan_tags: True/false if we delete unused Tags too
:param commit: True/False if this should end the db transaction"""
# User's CollectionItems are automatically deleted via "cascade".
# Comments on this Media are deleted by cascade, hopefully.
@ -393,6 +393,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,6 +488,92 @@ class ProcessingMetaData(Base):
"""A dict like view on this object"""
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'
}
class ReportBase(Base):
"""
@ -672,20 +762,38 @@ class PrivilegeUserAssociation(Base):
ForeignKey(Privilege.id),
primary_key=True)
with_polymorphic(
Notification,
[ProcessingNotification, CommentNotification])
privilege_foundations = [[u'admin'], [u'moderator'], [u'uploader'],[u'reporter'], [u'commenter'] ,[u'active']]
MODELS = [
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData, ReportBase,
CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation,
ArchivedReport]
ArchivedReport, Notification, CommentNotification,
ProcessingNotification, CommentSubscription]
# Foundations are the default rows that are created immediately after the tables are initialized. Each entry to
# this dictionary should be in the format of
# ModelObject:List of Rows
# (Each Row must be a list of parameters that can create and instance of the ModelObject)
#
"""
Foundations are the default rows that are created immediately after the tables
are initialized. Each entry to this dictionary should be in the format of:
ModelConstructorObject:List of Dictionaries
(Each Dictionary represents a row on the Table to be created, containing each
of the columns' names as a key string, and each of the columns' values as a
value)
ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
user_foundations = [{'name':u'Joanna', 'age':24},
{'name':u'Andrea', 'age':41}]
FOUNDATIONS = {User:user_foundations}
"""
privilege_foundations = [{'privilege_name':u'admin'},
{'privilege_name':u'moderator'},
{'privilege_name':u'uploader'},
{'privilege_name':u'reporter'},
{'privilege_name':u'commenter'},
{'privilege_name':u'active'}]
FOUNDATIONS = {Privilege:privilege_foundations}
######################################################

View File

@ -18,6 +18,29 @@
TODO: indexes on foreignkeys, where useful.
"""
###########################################################################
# WHAT IS THIS FILE?
# ------------------
#
# Upon occasion, someone runs into this file and wonders why we have
# both a models.py and a models_v0.py.
#
# The short of it is: you can ignore this file.
#
# The long version is, in two parts:
#
# - We used to use MongoDB, then we switched to SQL and SQLAlchemy.
# We needed to convert peoples' databases; the script we had would
# switch them to the first version right after Mongo, convert over
# all their tables, then run any migrations that were added after.
#
# - That script is now removed, but there is some discussion of
# writing a test that would set us at the first SQL migration and
# run everything after. If we wrote that, this file would still be
# useful. But for now, it's legacy!
#
###########################################################################
import datetime
import sys

View File

@ -52,10 +52,6 @@ class DatabaseMaster(object):
def load_models(app_config):
import mediagoblin.db.models
for media_type in app_config['media_types']:
_log.debug("Loading %s.models", media_type)
__import__(media_type + ".models")
for plugin in mg_globals.global_config.get('plugins', {}).keys():
_log.debug("Loading %s.models", plugin)
try:

View File

@ -25,7 +25,7 @@ from mediagoblin.db.models import (MediaEntry, Tag, MediaTag, Collection, \
def atomic_update(table, query_dict, update_values):
table.find(query_dict).update(update_values,
table.query.filter_by(**query_dict).update(update_values,
synchronize_session=False)
Session.commit()

View File

@ -21,9 +21,11 @@ from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_quote
from mediagoblin import mg_globals as mgg
from mediagoblin.db.models import MediaEntry, User, MediaComment, Privilege, \
UserBan
from mediagoblin.tools.response import redirect, render_404, render_user_banned
from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User, MediaComment,
UserBan
from mediagoblin.tools.response import redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
def require_active_login(controller):
@ -107,8 +109,8 @@ def user_may_alter_collection(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
creator_id = request.db.User.find_one(
{'username': request.matchdict['user']}).id
creator_id = request.db.User.query.filter_by(
username=request.matchdict['user']).first().id
if not (request.user.is_admin or
request.user.id == creator_id):
raise Forbidden()
@ -182,15 +184,15 @@ def get_user_collection(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
user = request.db.User.find_one(
{'username': request.matchdict['user']})
user = request.db.User.query.filter_by(
username=request.matchdict['user']).first()
if not user:
return render_404(request)
collection = request.db.Collection.find_one(
{'slug': request.matchdict['collection'],
'creator': user.id})
collection = request.db.Collection.query.filter_by(
slug=request.matchdict['collection'],
creator=user.id).first()
# Still no collection? Okay, 404.
if not collection:
@ -207,14 +209,14 @@ def get_user_collection_item(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
user = request.db.User.find_one(
{'username': request.matchdict['user']})
user = request.db.User.query.filter_by(
username=request.matchdict['user']).first()
if not user:
return render_404(request)
collection_item = request.db.CollectionItem.find_one(
{'id': request.matchdict['collection_item'] })
collection_item = request.db.CollectionItem.query.filter_by(
id=request.matchdict['collection_item']).first()
# Still no collection item? Okay, 404.
if not collection_item:
@ -247,6 +249,21 @@ def get_media_entry_by_id(controller):
return wrapper
def allow_registration(controller):
""" Decorator for if registration is enabled"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
if not mgg.app_config["allow_registration"]:
messages.add_message(
request,
messages.WARNING,
_('Sorry, registration is disabled on this instance.'))
return redirect(request, "index")
return controller(request, *args, **kwargs)
return wrapper
def get_media_comment_by_id(controller):
"""
Pass in a MediaComment based off of a url component
@ -264,15 +281,26 @@ def get_media_comment_by_id(controller):
return wrapper
def auth_enabled(controller):
"""Decorator for if an auth plugin is enabled"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
if not mgg.app.auth:
messages.add_message(
request,
messages.WARNING,
_('Sorry, authentication is disabled on this instance.'))
return redirect(request, 'index')
return controller(request, *args, **kwargs)
return wrapper
def get_workbench(func):
"""Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
@wraps(func)
def new_func(*args, **kwargs):
with mgg.workbench_manager.create() as workbench:
return func(*args, workbench=workbench, **kwargs)
return new_func
def require_admin_or_moderator_login(controller):

View File

@ -16,9 +16,11 @@
import wtforms
from mediagoblin.tools.text import tag_length_validator, TOO_LONG_TAG_WARNING
from mediagoblin.tools.text import tag_length_validator
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.tools.licenses import licenses_as_choices
from mediagoblin.auth.forms import normalize_user_or_email_field
class EditForm(wtforms.Form):
title = wtforms.TextField(
@ -59,6 +61,12 @@ class EditProfileForm(wtforms.Form):
class EditAccountForm(wtforms.Form):
new_email = wtforms.TextField(
_('New email address'),
[wtforms.validators.Optional(),
normalize_user_or_email_field(allow_user=False)])
wants_comment_notification = wtforms.BooleanField(
description=_("Email me when others comment on my media"))
license_preference = wtforms.SelectField(
_('License preference'),
[
@ -67,8 +75,6 @@ class EditAccountForm(wtforms.Form):
],
choices=licenses_as_choices(),
description=_('This will be your default license on upload forms.'))
wants_comment_notification = wtforms.BooleanField(
label=_("Email me when others comment on my media"))
class EditAttachmentsForm(wtforms.Form):

View File

@ -26,3 +26,5 @@ add_route('mediagoblin.edit.delete_account', '/edit/account/delete/',
'mediagoblin.edit.views:delete_account')
add_route('mediagoblin.edit.pass', '/edit/password/',
'mediagoblin.edit.views:change_pass')
add_route('mediagoblin.edit.verify_email', '/edit/verify_email/',
'mediagoblin.edit.views:verify_email')

View File

@ -16,25 +16,31 @@
from datetime import datetime
from itsdangerous import BadSignature
from werkzeug.exceptions import Forbidden
from werkzeug.utils import secure_filename
from mediagoblin import messages
from mediagoblin import mg_globals
from mediagoblin.auth import lib as auth_lib
from mediagoblin import auth
from mediagoblin.auth import tools as auth_tools
from mediagoblin.edit import forms
from mediagoblin.edit.lib import may_edit_media
from mediagoblin.decorators import (require_active_login, active_user_from_url,
get_media_entry_by_id,
user_may_alter_collection, get_user_collection)
from mediagoblin.tools.response import render_to_response, \
redirect, redirect_obj
get_media_entry_by_id, user_may_alter_collection,
get_user_collection)
from mediagoblin.tools.crypto import get_timed_signer_url
from mediagoblin.tools.mail import email_debug_message
from mediagoblin.tools.response import (render_to_response,
redirect, redirect_obj, render_404)
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.template import render_template
from mediagoblin.tools.text import (
convert_to_tag_list_of_dicts, media_tags_as_string)
from mediagoblin.tools.url import slugify
from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
from mediagoblin.db.models import User
import mimetypes
@ -212,6 +218,10 @@ def edit_profile(request, url_user=None):
{'user': user,
'form': form})
EMAIL_VERIFICATION_TEMPLATE = (
u'{uri}?'
u'token={verification_key}')
@require_active_login
def edit_account(request):
@ -220,27 +230,22 @@ def edit_account(request):
wants_comment_notification=user.wants_comment_notification,
license_preference=user.license_preference)
if request.method == 'POST':
form_validated = form.validate()
if request.method == 'POST' and form.validate():
user.wants_comment_notification = form.wants_comment_notification.data
if form_validated and \
form.wants_comment_notification.validate(form):
user.wants_comment_notification = \
form.wants_comment_notification.data
user.license_preference = form.license_preference.data
if form_validated and \
form.license_preference.validate(form):
user.license_preference = \
form.license_preference.data
if form.new_email.data:
_update_email(request, form, user)
if form_validated and not form.errors:
if not form.errors:
user.save()
messages.add_message(request,
messages.SUCCESS,
_("Account settings saved"))
messages.SUCCESS,
_("Account settings saved"))
return redirect(request,
'mediagoblin.user_pages.user_home',
user=user.username)
'mediagoblin.user_pages.user_home',
user=user.username)
return render_to_response(
request,
@ -300,9 +305,9 @@ def edit_collection(request, collection):
form.slug.data, collection.id)
# Make sure there isn't already a Collection with this title
existing_collection = request.db.Collection.find_one({
'creator': request.user.id,
'title':form.title.data})
existing_collection = request.db.Collection.query.filter_by(
creator=request.user.id,
title=form.title.data).first()
if existing_collection and existing_collection.id != collection.id:
messages.add_message(
@ -337,12 +342,16 @@ def edit_collection(request, collection):
@require_active_login
def change_pass(request):
# If no password authentication, no need to change your password
if 'pass_auth' not in request.template_env.globals:
return redirect(request, 'index')
form = forms.ChangePassForm(request.form)
user = request.user
if request.method == 'POST' and form.validate():
if not auth_lib.bcrypt_check_password(
if not auth.check_password(
form.old_password.data, user.pw_hash):
form.old_password.errors.append(
_('Wrong password'))
@ -354,7 +363,7 @@ def change_pass(request):
'user': user})
# Password matches
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
user.pw_hash = auth.gen_password_hash(
form.new_password.data)
user.save()
@ -369,3 +378,77 @@ def change_pass(request):
'mediagoblin/edit/change_pass.html',
{'form': form,
'user': user})
def verify_email(request):
"""
Email verification view for changing email address
"""
# If no token, we can't do anything
if not 'token' in request.GET:
return render_404(request)
# Catch error if token is faked or expired
token = None
try:
token = get_timed_signer_url("mail_verification_token") \
.loads(request.GET['token'], max_age=10*24*3600)
except BadSignature:
messages.add_message(
request,
messages.ERROR,
_('The verification key or user id is incorrect.'))
return redirect(
request,
'index')
user = User.query.filter_by(id=int(token['user'])).first()
if user:
user.email = token['email']
user.save()
messages.add_message(
request,
messages.SUCCESS,
_('Your email address has been verified.'))
else:
messages.add_message(
request,
messages.ERROR,
_('The verification key or user id is incorrect.'))
return redirect(
request, 'mediagoblin.user_pages.user_home',
user=user.username)
def _update_email(request, form, user):
new_email = form.new_email.data
users_with_email = User.query.filter_by(
email=new_email).count()
if users_with_email:
form.new_email.errors.append(
_('Sorry, a user with that email address'
' already exists.'))
elif not users_with_email:
verification_key = get_timed_signer_url(
'mail_verification_token').dumps({
'user': user.id,
'email': new_email})
rendered_email = render_template(
request, 'mediagoblin/edit/verification.txt',
{'username': user.username,
'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
uri=request.urlgen('mediagoblin.edit.verify_email',
qualified=True),
verification_key=verification_key)})
email_debug_message(request)
auth_tools.send_verification_email(user, request, new_email,
rendered_email)

View File

@ -32,17 +32,18 @@ def dbupdate_parse_setup(subparser):
class DatabaseData(object):
def __init__(self, name, models, migrations):
def __init__(self, name, models, foundations, migrations):
self.name = name
self.models = models
self.foundations = foundations
self.migrations = migrations
def make_migration_manager(self, session):
return MigrationManager(
self.name, self.models, self.migrations, session)
self.name, self.models, self.foundations, self.migrations, session)
def gather_database_data(media_types, plugins):
def gather_database_data(plugins):
"""
Gather all database data relevant to the extensions we have
installed so we can do migrations and table initialization.
@ -54,17 +55,11 @@ def gather_database_data(media_types, plugins):
# Add main first
from mediagoblin.db.models import MODELS as MAIN_MODELS
from mediagoblin.db.migrations import MIGRATIONS as MAIN_MIGRATIONS
from mediagoblin.db.models import FOUNDATIONS as MAIN_FOUNDATIONS
managed_dbdata.append(
DatabaseData(
u'__main__', MAIN_MODELS, MAIN_MIGRATIONS))
# Then get all registered media managers (eventually, plugins)
for media_type in media_types:
models = import_component('%s.models:MODELS' % media_type)
migrations = import_component('%s.migrations:MIGRATIONS' % media_type)
managed_dbdata.append(
DatabaseData(media_type, models, migrations))
u'__main__', MAIN_MODELS, MAIN_FOUNDATIONS, MAIN_MIGRATIONS))
for plugin in plugins:
try:
@ -90,13 +85,26 @@ forgotten to add it? ({1})'.format(plugin, exc))
migrations = {}
except AttributeError as exc:
_log.debug('Cloud not find MIGRATIONS in {0}.migrations, have you \
_log.debug('Could not find MIGRATIONS in {0}.migrations, have you \
forgotten to add it? ({1})'.format(plugin, exc))
migrations = {}
try:
foundations = import_component('{0}.models:FOUNDATIONS'.format(plugin))
except ImportError as exc:
_log.debug('No foundations found for {0}: {1}'.format(
plugin,
exc))
foundations = []
except AttributeError as exc:
_log.debug('Could not find FOUNDATIONS in {0}.models, have you \
forgotten to add it? ({1})'.format(plugin, exc))
foundations = {}
if models:
managed_dbdata.append(
DatabaseData(plugin, models, migrations))
DatabaseData(plugin, models, foundations, migrations))
return managed_dbdata
@ -110,13 +118,24 @@ def run_dbupdate(app_config, global_config):
in the future, plugins)
"""
# Gather information from all media managers / projects
dbdatas = gather_database_data(
app_config['media_types'],
global_config.get('plugins', {}).keys())
# Set up the database
db = setup_connection_and_db_from_config(app_config, migrations=True)
#Run the migrations
run_all_migrations(db, app_config, global_config)
def run_all_migrations(db, app_config, global_config):
"""
Initializes or migrates a database that already has a
connection setup and also initializes or migrates all
extensions based on the config files.
It can be used to initialize an in-memory database for
testing.
"""
# Gather information from all media managers / projects
dbdatas = gather_database_data(
global_config.get('plugins', {}).keys())
Session = sessionmaker(bind=db.engine)

View File

@ -63,7 +63,7 @@ def _import_media(db, args):
# TODO: Add import of queue files
queue_cache = BasicFileStorage(args._cache_path['queue'])
for entry in db.MediaEntry.find():
for entry in db.MediaEntry.query.filter_by():
for name, path in entry.media_files.items():
_log.info('Importing: {0} - {1}'.format(
entry.title.encode('ascii', 'replace'),
@ -204,7 +204,7 @@ def _export_media(db, args):
# TODO: Add export of queue files
queue_cache = BasicFileStorage(args._cache_path['queue'])
for entry in db.MediaEntry.find():
for entry in db.MediaEntry.query.filter_by():
for name, path in entry.media_files.items():
_log.info(u'Exporting {0} - {1}'.format(
entry.title,

View File

@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.gmg_commands import util as commands_util
from mediagoblin.auth import lib as auth_lib
from mediagoblin import auth
from mediagoblin import mg_globals
def adduser_parser_setup(subparser):
@ -40,9 +40,9 @@ def adduser(args):
db = mg_globals.database
users_with_username = \
db.User.find({
'username': args.username.lower(),
}).count()
db.User.query.filter_by(
username=args.username.lower()
).count()
if users_with_username:
print u'Sorry, a user with that name already exists.'
@ -52,7 +52,7 @@ def adduser(args):
entry = db.User()
entry.username = unicode(args.username.lower())
entry.email = unicode(args.email)
entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
entry.pw_hash = auth.gen_password_hash(args.password)
entry.status = u'active'
entry.email_verified = True
default_privileges = [
@ -78,7 +78,8 @@ def makeadmin(args):
db = mg_globals.database
user = db.User.one({'username': unicode(args.username.lower())})
user = db.User.query.filter_by(
username=unicode(args.username.lower())).one()
if user:
user.is_admin = True
user.all_privileges.append(
@ -105,9 +106,10 @@ def changepw(args):
db = mg_globals.database
user = db.User.one({'username': unicode(args.username.lower())})
user = db.User.query.filter_by(
username=unicode(args.username.lower())).one()
if user:
user.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
user.pw_hash = auth.gen_password_hash(args.password)
user.save()
print 'Password successfully changed'
else:

View File

@ -15,15 +15,15 @@
# Elrond <elrond+mediagoblin.org@samba-tng.org>, 2011
# Art O. Pal <artopal@fastmail.fm>, 2011
# spaetz <sebastian@sspaeth.de>, 2012
# Vinzenz Vietzke <vietzke@b1-systems.de>, 2012
# Vinzenz Vietzke <vietzke@b1-systems.de>, 2011
# Vinzenz Vietzke <vinz@vinzv.de>, 2012
# Vinzenz Vietzke <vinz@vinzv.de>, 2011
msgid ""
msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-05-28 10:43+0000\n"
"Last-Translator: Elrond <elrond+mediagoblin.org@samba-tng.org>\n"
"Language-Team: German (http://www.transifex.com/projects/p/mediagoblin/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -47,7 +47,7 @@ msgstr "E-Mail-Adresse"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Benutzername oder E-Mail-Adresse"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -628,7 +628,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Registriere dich auf dieser Seite</a> oder <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Installiere MediaGoblin auf deinem eigenen Server</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -784,7 +784,7 @@ msgstr "Bild für %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF-Datei"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -944,11 +944,11 @@ msgstr ""
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Hinzugefügt"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Originaldatum"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1136,27 +1136,27 @@ msgstr "Tut uns Leid, aber unter der angegebenen Adresse gibt es keine Seite!</p
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "Jahr"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "Monat"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "Woche"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "Tag"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "Stunde"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "Minute"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"POT-Creation-Date: 2013-06-16 20:06-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,94 +17,94 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"
#: mediagoblin/auth/forms.py:26
#: mediagoblin/auth/forms.py:25
msgid "Username"
msgstr ""
#: mediagoblin/auth/forms.py:30 mediagoblin/auth/forms.py:45
#: mediagoblin/auth/forms.py:29 mediagoblin/auth/forms.py:44
#: mediagoblin/tests/test_util.py:110
msgid "Password"
msgstr ""
#: mediagoblin/auth/forms.py:34
#: mediagoblin/auth/forms.py:33
msgid "Email address"
msgstr ""
#: mediagoblin/auth/forms.py:41
#: mediagoblin/auth/forms.py:40
msgid "Username or Email"
msgstr ""
#: mediagoblin/auth/forms.py:52
#: mediagoblin/auth/forms.py:51
msgid "Username or email"
msgstr ""
#: mediagoblin/auth/tools.py:31
#: mediagoblin/auth/tools.py:42
msgid "Invalid User name or email address."
msgstr ""
#: mediagoblin/auth/tools.py:32
#: mediagoblin/auth/tools.py:43
msgid "This field does not take email addresses."
msgstr ""
#: mediagoblin/auth/tools.py:33
#: mediagoblin/auth/tools.py:44
msgid "This field requires an email address."
msgstr ""
#: mediagoblin/auth/views.py:54
msgid "Sorry, registration is disabled on this instance."
msgstr ""
#: mediagoblin/auth/views.py:68
#: mediagoblin/auth/tools.py:109
msgid "Sorry, a user with that name already exists."
msgstr ""
#: mediagoblin/auth/views.py:72
#: mediagoblin/auth/tools.py:113
msgid "Sorry, a user with that email address already exists."
msgstr ""
#: mediagoblin/auth/views.py:182
#: mediagoblin/auth/views.py:43
msgid "Sorry, registration is disabled on this instance."
msgstr ""
#: mediagoblin/auth/views.py:133
msgid ""
"Your email address has been verified. You may now login, edit your "
"profile, and submit images!"
msgstr ""
#: mediagoblin/auth/views.py:188
#: mediagoblin/auth/views.py:139
msgid "The verification key or user id is incorrect"
msgstr ""
#: mediagoblin/auth/views.py:206
#: mediagoblin/auth/views.py:157
msgid "You must be logged in so we know who to send the email to!"
msgstr ""
#: mediagoblin/auth/views.py:214
#: mediagoblin/auth/views.py:165
msgid "You've already verified your email address!"
msgstr ""
#: mediagoblin/auth/views.py:227
#: mediagoblin/auth/views.py:178
msgid "Resent your verification email."
msgstr ""
#: mediagoblin/auth/views.py:258
#: mediagoblin/auth/views.py:209
msgid ""
"If that email address (case sensitive!) is registered an email has been "
"sent with instructions on how to change your password."
msgstr ""
#: mediagoblin/auth/views.py:269
#: mediagoblin/auth/views.py:220
msgid "Couldn't find someone with that username."
msgstr ""
#: mediagoblin/auth/views.py:272
#: mediagoblin/auth/views.py:223
msgid "An email has been sent with instructions on how to change your password."
msgstr ""
#: mediagoblin/auth/views.py:279
#: mediagoblin/auth/views.py:230
msgid ""
"Could not send password recovery email as your username is inactive or "
"your account's email address has not been verified."
msgstr ""
#: mediagoblin/auth/views.py:336
#: mediagoblin/auth/views.py:287
msgid "You can now log in using your new password."
msgstr ""
@ -634,13 +634,13 @@ msgid "Editing attachments for %(media_title)s"
msgstr ""
#: mediagoblin/templates/mediagoblin/edit/attachments.html:44
#: mediagoblin/templates/mediagoblin/user_pages/media.html:182
#: mediagoblin/templates/mediagoblin/user_pages/media.html:198
#: mediagoblin/templates/mediagoblin/user_pages/media.html:171
#: mediagoblin/templates/mediagoblin/user_pages/media.html:187
msgid "Attachments"
msgstr ""
#: mediagoblin/templates/mediagoblin/edit/attachments.html:57
#: mediagoblin/templates/mediagoblin/user_pages/media.html:204
#: mediagoblin/templates/mediagoblin/user_pages/media.html:193
msgid "Add attachment"
msgstr ""
@ -763,6 +763,17 @@ msgstr ""
msgid "WebM file (Vorbis codec)"
msgstr ""
#: mediagoblin/templates/mediagoblin/media_displays/image.html:36
msgid "Created"
msgstr ""
#: mediagoblin/templates/mediagoblin/media_displays/image.html:39
#: mediagoblin/templates/mediagoblin/user_pages/media.html:132
#: mediagoblin/templates/mediagoblin/user_pages/media.html:152
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:59
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:87
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:93
@ -928,21 +939,10 @@ msgstr ""
msgid "Add this comment"
msgstr ""
#: mediagoblin/templates/mediagoblin/user_pages/media.html:132
#: mediagoblin/templates/mediagoblin/user_pages/media.html:152
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
#, python-format

View File

@ -12,8 +12,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-06-01 21:16+0000\n"
"Last-Translator: aleksejrs <deletesoftware@yandex.ru>\n"
"Language-Team: Esperanto (http://www.transifex.com/projects/p/mediagoblin/language/eo/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -37,7 +37,7 @@ msgstr "Retpoŝtadreso"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Uzantonomo aŭ retpoŝtadreso"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -178,7 +178,7 @@ msgstr "Permesila prefero"
#: mediagoblin/edit/forms.py:69
msgid "This will be your default license on upload forms."
msgstr ""
msgstr "Tiu ĉi permesilo estos antaŭelektita en la alŝutformularoj."
#: mediagoblin/edit/forms.py:71
msgid "Email me when others comment on my media"
@ -264,7 +264,7 @@ msgstr "Malĝusta pasvorto"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Via pasvorto estas sukcese ŝanĝita"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -665,11 +665,11 @@ msgstr "Konservi ŝanĝojn"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Ŝanĝado de pasvorto de %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Konservi"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -700,7 +700,7 @@ msgstr "Ŝanĝado de kontagordoj de %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Ŝanĝi la pasvorton"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -774,7 +774,7 @@ msgstr "Bildo de «%(media_title)s»"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF-dosiero"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -930,15 +930,15 @@ msgstr "Aldoni ĉi tiun komenton"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "antaŭ %(formatted_time)s"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Aldonita"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Kreita"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1126,27 +1126,27 @@ msgstr ""
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "jaro(j)"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "monato(j)"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "semajno(j)"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "tago(j)"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "horo(j)"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "minuto(j)"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1185,7 +1185,7 @@ msgstr "komentis je via afiŝo"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Ve, komentado estas malebligita."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -20,8 +20,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-06-02 21:23+0000\n"
"Last-Translator: larjona <larjona99@gmail.com>\n"
"Language-Team: Spanish (http://www.transifex.com/projects/p/mediagoblin/language/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -45,7 +45,7 @@ msgstr "Dirección de correo electrónico"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Nombre de usuario o correo electrónico"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -272,7 +272,7 @@ msgstr "Contraseña incorrecta"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Se ha cambiado la contraseña correctamente"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -289,17 +289,17 @@ msgstr "Sin embargo, se encontró un enlace simbólico de un directorio antiguo;
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "No se pudo enlazar \"%s\": %s existe y no es un enlace simbólico\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "Omitiendo \"%s\"; ya está establecido.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "Se encontró un enlace antiguo para \"%s\"; se eliminará.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -315,7 +315,7 @@ msgstr "Lo sentidos, No soportamos ese tipo de archivo :("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "ha fallado la ejecución de unoconv, comprueba el fichero de registro (log)"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -626,7 +626,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Crear una cuenta en este sitio</a>\n o\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Instalar MediaGoblin en tu propio servidor</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -673,11 +673,11 @@ msgstr "Guardar cambios"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Cambiando la contraseña de %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Guardar"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -708,7 +708,7 @@ msgstr "Cambio de %(username)s la configuración de la cuenta "
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Cambiar tu contraseña."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -782,7 +782,7 @@ msgstr "Imágenes para %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "Fichero PDF"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -938,15 +938,15 @@ msgstr "Añade un comentario "
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "hace %(formatted_time)s"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Agregado"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Creado"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1134,27 +1134,27 @@ msgstr "Parece que no hay ninguna página en esta dirección. ¡Lo siento!</p><p
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "año"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "mes"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "semana"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "día"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "hora"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "minuto"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1193,7 +1193,7 @@ msgstr "comentó tu publicación"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Lo siento, los comentarios están desactivados."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -11,8 +11,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-06-01 07:11+0000\n"
"Last-Translator: GenghisKhan <genghiskhan@gmx.ca>\n"
"Language-Team: Hebrew (http://www.transifex.com/projects/p/mediagoblin/language/he/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -36,7 +36,7 @@ msgstr "כתובת דוא״ל"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "שם משתמש או דוא״ל"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -263,7 +263,7 @@ msgstr "סיסמה שגויה"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "סיסמתך שונתה בהצלחה"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -280,17 +280,17 @@ msgstr "בכל אופן, קישור מדור symlink נמצא; הוסר.\n"
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "לא היתה אפשרות לקשר את \"%s\": %s קיים ואינו קישור סמלי (symlink)\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "מדלג על \"%s\"; כבר מוגדר.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "קישור ישן נמצא עבור \"%s\"; מסיר כעת.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -306,7 +306,7 @@ msgstr "צר לי, אינני תומך בטיפוס קובץ זה :("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "unoconv נכשל לפעול, בדוק קובץ יומן"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -617,7 +617,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">צור חשבון באתר זה</a>\n או\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">התקן את MediaGoblin על שרתך</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -664,11 +664,11 @@ msgstr "שמור שינויים"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "משנה כעת את הסיסמה של %(username)s'"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "שמור"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -699,7 +699,7 @@ msgstr "שינוי הגדרות חשבון עבור %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "שנה את סיסמתך."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -773,7 +773,7 @@ msgstr "תמונה עבור %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "קובץ PDF"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -929,15 +929,15 @@ msgstr "הוסף את תגובה זו"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "מלפני %(formatted_time)s"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "התווסף"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "נוצר"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1125,27 +1125,27 @@ msgstr "לא נראה שקיים עמוד בכתובת זו. צר לי!</p><p>א
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "שנה"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "חודש"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "שבוע"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "יום"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "שעה"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "דקה"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1184,7 +1184,7 @@ msgstr "הגיב/ה על פרסומך"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "מצטערים, תגובות מנוטרלות."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-06-05 22:51+0000\n"
"Last-Translator: tryggvib <tryggvib@fsfi.is>\n"
"Language-Team: Icelandic (Iceland) (http://www.transifex.com/projects/p/mediagoblin/language/is_IS/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -35,7 +35,7 @@ msgstr "Netfang"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Notandanafn eða tölvupóstur"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -262,7 +262,7 @@ msgstr "Vitlaust lykilorð"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Það tókst að breyta lykilorðinu þínu"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -279,17 +279,17 @@ msgstr "Fann samt gamlan táknrænan tengil á möppu; fjarlægður.\n"
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "Gat ekki tengt \"%s\": %s er til og er ekki sýndartengill\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "Hoppa yfir \"%s\"; hefur nú þegar verið sett upp.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "Gamall tengill fannst fyrir \"%s\"; fjarlægi.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -305,7 +305,7 @@ msgstr "Ég styð því miður ekki þessa gerð af skrám :("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "tekst ekki að keyra unoconv, athugaðu annálsskrá"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -616,7 +616,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Búa til aðgang á þessari síðu</a>\neða\n<a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Settu upp þinn eigin margmiðlunarþjón</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -663,11 +663,11 @@ msgstr "Vista breytingar"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Breyti lykilorði fyrir notandann: %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Vista"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -698,7 +698,7 @@ msgstr "Breyti notandaaðgangsstillingum fyrir: %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Breyta lykilorðinu þínu."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -772,7 +772,7 @@ msgstr "Mynd fyrir %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF skrá"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -928,15 +928,15 @@ msgstr "Senda inn þessa athugasemd"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "Fyrir %(formatted_time)s"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Bætt við"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Skapað"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1124,27 +1124,27 @@ msgstr "Því miður! Það virðist ekki vera nein síða á þessari vefslóð
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "ár"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "mánuður"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "vika"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "dagur"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "klukkustund"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "mínúta"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1183,7 +1183,7 @@ msgstr "skrifaði athugasemd við færsluna þína"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Því miður, athugasemdir eru óvirkar."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-05-31 15:40+0000\n"
"Last-Translator: velmont <odin.omdal@gmail.com>\n"
"Language-Team: Norwegian Nynorsk (Norway) (http://www.transifex.com/projects/p/mediagoblin/language/nn_NO/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -35,7 +35,7 @@ msgstr "Epost"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Brukarnamn eller epost"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -262,7 +262,7 @@ msgstr "Feil passord"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Endra passord"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -279,17 +279,17 @@ msgstr "However, old link directory symlink found; removed.\n"
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "Kunne ikkje lenkja «%s»: %s eksisterer og er ikkje ei symlenkje\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "Hopper over «%s»: allereie satt opp.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "Gamal lenkje funnen for «%s»; fjernar.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -305,7 +305,7 @@ msgstr "Orsak, stør ikkje den filtypen :("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "klarte ikkje køyra unoconv, sjekk logg-fil"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -616,7 +616,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Opprett ein konto på denne sida</a>\n eller\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set opp din eigen MediaGoblin-server</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -663,11 +663,11 @@ msgstr "Lagra"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Endrar passordet til %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Lagra"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -698,7 +698,7 @@ msgstr "Endrar kontoinnstellingane til %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Endra passordet ditt."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -772,7 +772,7 @@ msgstr "Bilete for %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF-fil"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -928,15 +928,15 @@ msgstr "Legg til dette innspelet"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "%(formatted_time)s sidan"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Lagt til"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Oppretta"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1124,27 +1124,27 @@ msgstr "Ser ikkje ut til å finnast noko her. Orsak.</p>\n<p>Dersom du er sikker
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "år"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "månad"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "veke"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "dag"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "time"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "minutt"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1183,7 +1183,7 @@ msgstr "kom med innspel på innlegget ditt"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Innspel er avslege"
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-05-28 13:51+0000\n"
"Last-Translator: Sergiusz Pawlowicz <transifex@pawlowicz.name>\n"
"Language-Team: Polish (http://www.transifex.com/projects/p/mediagoblin/language/pl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -35,7 +35,7 @@ msgstr "Adres e-mail"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Nazwa konta lub adres poczty elektronicznej"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -262,7 +262,7 @@ msgstr "Nieprawidłowe hasło"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Twoje hasło zostało zmienione"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -279,17 +279,17 @@ msgstr "Znaleziono stary odnośnik symboliczny do katalogu; usunięto.\n"
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "Nie mogę zrobić odnośnika \"%s\": %s istnieje i nie jest odnośnikiem\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "Opuszczam \"%s\"; już jest gotowe.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "Znaleziono stary odnośnik dla \"%s\"; usuwam.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -305,7 +305,7 @@ msgstr "NIestety, nie obsługujemy tego typu plików :-("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "nie dało się uruchomić unoconv, sprawdź log"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -616,7 +616,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Załóż konto na tym serwerze</a>\n albo\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Uruchom MediaGoblin na swoim własnym serwerze</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -663,11 +663,11 @@ msgstr "Zapisz zmiany"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Zmieniam hasło użytkownika %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Zachowaj"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -698,7 +698,7 @@ msgstr "Zmiana ustawień konta %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Zmień swoje hasło."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -772,7 +772,7 @@ msgstr "Grafika dla %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "Plik PDF"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -928,15 +928,15 @@ msgstr "Dodaj komentarz"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "%(formatted_time)s temu"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Dodano"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Utworzono"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1124,27 +1124,27 @@ msgstr "Wygląda na to, że nic tutaj nie ma!</p><p>Jeśli jesteś pewny, że ad
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "rok"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "miesiąc"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "tydzień"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "dzień"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "godzina"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "minuta"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1183,7 +1183,7 @@ msgstr "komentarze do twojego wpisu"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Komentowanie jest wyłączone."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-05-27 20:40+0000\n"
"Last-Translator: George Pop <gapop@hotmail.com>\n"
"Language-Team: Romanian (http://www.transifex.com/projects/p/mediagoblin/language/ro/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -35,7 +35,7 @@ msgstr "Adresa de e-mail"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Numele de utilizator sau adresa de e-mail"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -262,7 +262,7 @@ msgstr "Parolă incorectă"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Parola a fost schimbată cu succes"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -279,17 +279,17 @@ msgstr "A fost însă găsit un symlink către vechiul folder; s-a șters.\n"
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "Nu s-a putut crea link pentru \"%s\": %s există deja și nu este symlink\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "S-a omis \"%s\"; configurat deja.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "Există deja un link pentru \"%s\"; va fi șters.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -305,7 +305,7 @@ msgstr "Scuze, nu recunosc acest tip de fișier :("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "unoconv nu poate fi executat; verificați log-ul"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -616,7 +616,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Creați un cont pe acest site</a>\n sau\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Instalați MediaGoblin pe serverul dvs.</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -663,11 +663,11 @@ msgstr "Salvează modificările"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Se modifică parola pentru %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Salvează"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -698,7 +698,7 @@ msgstr "Se modifică setările contului pentru userul %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Modifică parolă."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -772,7 +772,7 @@ msgstr "Imagine pentru %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "Fișier PDF"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -928,15 +928,15 @@ msgstr "Trimite acest comentariu"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "în urmă cu %(formatted_time)s"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Adăugat"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Creat"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1124,27 +1124,27 @@ msgstr "Nu există nicio pagină la această adresă.</p><p>Dacă sunteți sigur
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "anul"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "luna"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "săptămâna"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "ziua"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "ora"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "minutul"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1183,7 +1183,7 @@ msgstr "a făcut un comentariu la postarea ta"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Comentariile sunt dezactivate."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-06-01 21:08+0000\n"
"Last-Translator: aleksejrs <deletesoftware@yandex.ru>\n"
"Language-Team: Russian (http://www.transifex.com/projects/p/mediagoblin/language/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -35,7 +35,7 @@ msgstr "Адрес электронной почты"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Имя пользователя или адрес электронной почты"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -262,7 +262,7 @@ msgstr "Неправильный пароль"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Ваш пароль сменён успешно"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -663,11 +663,11 @@ msgstr "Сохранить изменения"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Смена пароля %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Сохранить"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -698,7 +698,7 @@ msgstr "Настройка учётной записи %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Сменить пароль"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -772,7 +772,7 @@ msgstr "Изображение «%(media_title)s»"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF-файл"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -928,15 +928,15 @@ msgstr "Добавить этот комментарий"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "%(formatted_time)s назад"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Добавлен"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Создан"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1144,7 +1144,7 @@ msgstr ""
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "мин"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1183,7 +1183,7 @@ msgstr "оставил комментарий к вашему файлу"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Сожалеем: возможность комментирования отключена."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -14,8 +14,8 @@ msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-05-28 07:47+0000\n"
"Last-Translator: martin <zatroch.martin@gmail.com>\n"
"Language-Team: Slovak (http://www.transifex.com/projects/p/mediagoblin/language/sk/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -39,7 +39,7 @@ msgstr "Email adresse"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "Použivateľské meno alebo e-mail"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -266,7 +266,7 @@ msgstr "Nesprávne heslo"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "Tvoje heslo bolo úspešne zmenené"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -283,17 +283,17 @@ msgstr "Odstránené; hoci bol pôvodný symbolický odkaz adresára nájdený.\
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "Nemožno odkazovať na \"%s\": %s existuje a nie je symbolickým odkazom\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "Preskakujem \"%s\"; opakovane nastavené.\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "Nájdený starý odkaz pre \"%s\"; odstraňujem.\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
@ -309,7 +309,7 @@ msgstr "Prepáč, nepodporujem tento typ súborov =("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "beh unoconv zlyhal, preskúmajte log záznam"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -620,7 +620,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">Vytvoriť účet na tejto stránke</a>\n alebo\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Nastaviť MediaGoblin na vlastnom serveri</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -667,11 +667,11 @@ msgstr "Uložiť zmeny"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "Mením heslo používateľa %(username)s"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "Uložiť"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
@ -702,7 +702,7 @@ msgstr "Mením nastavenia účtu používateľa %(username)s"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "Zmeniť svoje heslo."
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
@ -776,7 +776,7 @@ msgstr "Obrázok pre %(media_title)s"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF súbor"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -932,15 +932,15 @@ msgstr "Pridať tento komentár"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "pred %(formatted_time)s "
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "Pridané"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "Vytvorené"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
@ -1128,27 +1128,27 @@ msgstr "Zdá sa, že na tejto adrese sa nič nenachádza. Prepáč!</p><p>Pokia
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr "rok"
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr "mesiac"
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr "týždeň"
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr "deň"
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "hodina"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr "minúta"
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
@ -1187,7 +1187,7 @@ msgstr "okmentoval tvoj príspevok"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "Prepáč, komentovanie je vypnuté."
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,16 @@
# Translators:
# <chc@citi.sinica.edu.tw>, 2011
# Harry Chen <harryhow@gmail.com>, 2011-2012
# medicalwei <medicalwei@gmail.com>, 2013
# medicalwei <medicalwei@gmail.com>, 2012
# m13253 <m13253@hotmail.com>, 2013
msgid ""
msgstr ""
"Project-Id-Version: GNU MediaGoblin\n"
"Report-Msgid-Bugs-To: http://issues.mediagoblin.org/\n"
"POT-Creation-Date: 2013-05-27 13:54-0500\n"
"PO-Revision-Date: 2013-05-27 18:54+0000\n"
"Last-Translator: cwebber <cwebber@dustycloud.org>\n"
"PO-Revision-Date: 2013-06-16 01:40+0000\n"
"Last-Translator: m13253 <m13253@hotmail.com>\n"
"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/mediagoblin/language/zh_TW/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -36,7 +38,7 @@ msgstr "Email 位址"
#: mediagoblin/auth/forms.py:41
msgid "Username or Email"
msgstr ""
msgstr "使用者名稱或 email"
#: mediagoblin/auth/forms.py:52
msgid "Username or email"
@ -44,15 +46,15 @@ msgstr "使用者名稱或 email"
#: mediagoblin/auth/tools.py:31
msgid "Invalid User name or email address."
msgstr ""
msgstr "無效的使用者名稱或 email 位置。"
#: mediagoblin/auth/tools.py:32
msgid "This field does not take email addresses."
msgstr ""
msgstr "本欄位不接受 email 位置。"
#: mediagoblin/auth/tools.py:33
msgid "This field requires an email address."
msgstr ""
msgstr "本欄位需要 email 位置。"
#: mediagoblin/auth/views.py:54
msgid "Sorry, registration is disabled on this instance."
@ -92,11 +94,11 @@ msgstr "重送認證信。"
msgid ""
"If that email address (case sensitive!) is registered an email has been sent"
" with instructions on how to change your password."
msgstr ""
msgstr "如果那 email 位置 (請注意大小寫) 已經註冊,寫有修改密碼步驟的 email 已經送出。"
#: mediagoblin/auth/views.py:269
msgid "Couldn't find someone with that username."
msgstr ""
msgstr "找不到相關的使用者名稱。"
#: mediagoblin/auth/views.py:272
msgid ""
@ -173,15 +175,15 @@ msgstr "本網址出錯了"
#: mediagoblin/edit/forms.py:63
msgid "License preference"
msgstr ""
msgstr "授權偏好"
#: mediagoblin/edit/forms.py:69
msgid "This will be your default license on upload forms."
msgstr ""
msgstr "在上傳頁面,這將會是您預設的授權模式。"
#: mediagoblin/edit/forms.py:71
msgid "Email me when others comment on my media"
msgstr "當有人對我的媒體評論時寄信給我"
msgstr "當有人對我的媒體留言時寄信給我"
#: mediagoblin/edit/forms.py:83
msgid "The title can't be empty"
@ -225,7 +227,7 @@ msgstr "您加上了附件「%s」"
#: mediagoblin/edit/views.py:182
msgid "You can only edit your own profile."
msgstr ""
msgstr "您只能修改您自己的個人檔案。"
#: mediagoblin/edit/views.py:188
msgid "You are editing a user's profile. Proceed with caution."
@ -241,7 +243,7 @@ msgstr "帳號設定已儲存"
#: mediagoblin/edit/views.py:274
msgid "You need to confirm the deletion of your account."
msgstr ""
msgstr "您必須要確認是否刪除您的帳號。"
#: mediagoblin/edit/views.py:310 mediagoblin/submit/views.py:138
#: mediagoblin/user_pages/views.py:222
@ -263,7 +265,7 @@ msgstr "密碼錯誤"
#: mediagoblin/edit/views.py:363
msgid "Your password was changed successfully"
msgstr ""
msgstr "您的密碼已經成功修改"
#: mediagoblin/gmg_commands/assetlink.py:60
msgid "Cannot link theme... no theme set\n"
@ -280,24 +282,24 @@ msgstr "但是舊的目錄連結已經找到並移除。\n"
#: mediagoblin/gmg_commands/assetlink.py:112
#, python-format
msgid "Could not link \"%s\": %s exists and is not a symlink\n"
msgstr ""
msgstr "無法連結「%s」%s 存在,且不是符號連結\n"
#: mediagoblin/gmg_commands/assetlink.py:119
#, python-format
msgid "Skipping \"%s\"; already set up.\n"
msgstr ""
msgstr "跳過「%s」已經建置完成。\n"
#: mediagoblin/gmg_commands/assetlink.py:124
#, python-format
msgid "Old link found for \"%s\"; removing.\n"
msgstr ""
msgstr "找到「%s」舊的連結刪除中。\n"
#: mediagoblin/meddleware/csrf.py:134
msgid ""
"CSRF cookie not present. This is most likely the result of a cookie blocker "
"or somesuch.<br/>Make sure to permit the settings of cookies for this "
"domain."
msgstr ""
msgstr "跨網站存取 (CSRF) 的 cookie 不存在,有可能是 cookie 阻擋程式之類的程式導致的。<br/>請允許此網域的 cookie 設定。"
#: mediagoblin/media_types/__init__.py:111
#: mediagoblin/media_types/__init__.py:155
@ -306,7 +308,7 @@ msgstr "抱歉,我不支援這樣的檔案格式 :("
#: mediagoblin/media_types/pdf/processing.py:136
msgid "unoconv failing to run, check log file"
msgstr ""
msgstr "unoconv 無法執行,請檢查紀錄檔"
#: mediagoblin/media_types/video/processing.py:37
msgid "Video transcoding failed"
@ -335,7 +337,7 @@ msgstr "名稱"
#: mediagoblin/plugins/oauth/forms.py:35
msgid "The name of the OAuth client"
msgstr "OAuth client 的名稱"
msgstr "OAuth 用戶程式的名稱"
#: mediagoblin/plugins/oauth/forms.py:36
msgid "Description"
@ -359,7 +361,7 @@ msgid ""
" <strong>Public</strong> - The client can't make confidential\n"
" requests to the GNU MediaGoblin instance (e.g. client-side\n"
" JavaScript client)."
msgstr "<strong>秘密</strong> — OAuth client 可以對 GNU MediaGoblin 站台發送不被使用者代理攔截的請求 (例如伺服端的 client)。\n<strong>公開</strong> — OAuth client 無法對 GNU MediaGoblin 站台發送秘密的請求 (例如客戶端的 JavaScript client)。"
msgstr "<strong>秘密</strong> — OAuth 用戶程式可以對 GNU MediaGoblin 站台發送不被使用者代理攔截的請求 (例如伺服端的用戶程式)。\n<strong>公開</strong> — OAuth 用戶程式無法對 GNU MediaGoblin 站台發送秘密的請求 (例如客戶端的 JavaScript 用戶程式)。"
#: mediagoblin/plugins/oauth/forms.py:52
msgid "Redirect URI"
@ -369,23 +371,23 @@ msgstr "重定向 URI"
msgid ""
"The redirect URI for the applications, this field\n"
" is <strong>required</strong> for public clients."
msgstr "此應用程式的重定向 URI本欄位在公開類型的 OAuth client 為必填。"
msgstr "此應用程式的重定向 URI本欄位在公開類型的 OAuth 用戶程式為必填。"
#: mediagoblin/plugins/oauth/forms.py:66
msgid "This field is required for public clients"
msgstr "本欄位在公開類型的 OAuth client 為必填"
msgstr "本欄位在公開類型的用戶程式為必填"
#: mediagoblin/plugins/oauth/views.py:56
msgid "The client {0} has been registered!"
msgstr "OAuth client {0} 註冊完成!"
msgstr "OAuth 用戶程式 {0} 註冊完成!"
#: mediagoblin/plugins/oauth/templates/oauth/client/connections.html:22
msgid "OAuth client connections"
msgstr ""
msgstr "OAuth 用戶程式連線"
#: mediagoblin/plugins/oauth/templates/oauth/client/list.html:22
msgid "Your OAuth clients"
msgstr ""
msgstr "您的 OAuth 用戶程式"
#: mediagoblin/plugins/oauth/templates/oauth/client/register.html:29
#: mediagoblin/templates/mediagoblin/submit/collection.html:30
@ -450,7 +452,7 @@ msgstr "媒體處理面板"
#: mediagoblin/templates/mediagoblin/base.html:96
msgid "Log out"
msgstr ""
msgstr "登出"
#: mediagoblin/templates/mediagoblin/base.html:99
#: mediagoblin/templates/mediagoblin/user_pages/user.html:156
@ -577,7 +579,7 @@ msgstr "%(username)s 您好:\n\n要啟動 GNU MediaGoblin 帳號,請在您
msgid ""
"Powered by <a href=\"http://mediagoblin.org/\" title='Version "
"%(version)s'>MediaGoblin</a>, a <a href=\"http://gnu.org/\">GNU</a> project."
msgstr ""
msgstr "本站使用 <a href=\"http://mediagoblin.org/\" title='Version %(version)s'>MediaGoblin</a>,這是一個 <a href=\"http://gnu.org/\">GNU</a> 專案。"
#: mediagoblin/templates/mediagoblin/bits/base_footer.html:24
#, python-format
@ -617,7 +619,7 @@ msgid ""
"<a class=\"button_action_highlight\" href=\"%(register_url)s\">Create an account at this site</a>\n"
" or\n"
" <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">Set up MediaGoblin on your own server</a>"
msgstr ""
msgstr "<a class=\"button_action_highlight\" href=\"%(register_url)s\">在本站建立您的帳號</a>\n 或是\n <a class=\"button_action\" href=\"http://wiki.mediagoblin.org/HackingHowto\">在您自己的伺服器上安裝 MediaGoblin</a>"
#: mediagoblin/templates/mediagoblin/bits/logo.html:23
#: mediagoblin/themes/airy/templates/mediagoblin/bits/logo.html:23
@ -664,20 +666,20 @@ msgstr "儲存變更"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:38
#, python-format
msgid "Changing %(username)s's password"
msgstr ""
msgstr "更改 %(username)s 的密碼"
#: mediagoblin/templates/mediagoblin/edit/change_pass.html:45
msgid "Save"
msgstr ""
msgstr "儲存"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:28
#, python-format
msgid "Really delete user '%(user_name)s' and all related media/comments?"
msgstr ""
msgstr "真的要刪除使用者「%(user_name)s」以及相關的媒體與留言"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:35
msgid "Yes, really delete my account"
msgstr ""
msgstr "是的,我真的要把我的帳號刪除"
#: mediagoblin/templates/mediagoblin/edit/delete_account.html:44
#: mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html:48
@ -699,11 +701,11 @@ msgstr "正在改變 %(username)s 的帳號設定"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:46
msgid "Change your password."
msgstr ""
msgstr "更改您的密碼。"
#: mediagoblin/templates/mediagoblin/edit/edit_account.html:62
msgid "Delete my account"
msgstr ""
msgstr "刪除我的帳號"
#: mediagoblin/templates/mediagoblin/edit/edit_collection.html:29
#, python-format
@ -722,7 +724,7 @@ msgstr "編輯 %(username)s 的個人檔案"
#: mediagoblin/templates/mediagoblin/listings/tag.html:35
#, python-format
msgid "Media tagged with: %(tag_name)s"
msgstr "此媒體被 tag 成%(tag_name)s"
msgstr "這個媒體具有以下標籤%(tag_name)s"
#: mediagoblin/templates/mediagoblin/media_displays/ascii.html:34
#: mediagoblin/templates/mediagoblin/media_displays/audio.html:56
@ -773,7 +775,7 @@ msgstr " %(media_title)s 的照片"
#: mediagoblin/templates/mediagoblin/media_displays/pdf.html:79
msgid "PDF file"
msgstr ""
msgstr "PDF 檔"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:112
msgid "Toggle Rotate"
@ -781,7 +783,7 @@ msgstr "切換旋轉"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:113
msgid "Perspective"
msgstr "視"
msgstr "視"
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:116
#: mediagoblin/templates/mediagoblin/media_displays/stl.html:117
@ -820,14 +822,14 @@ msgid ""
"Sorry, this video will not work because\n"
" your web browser does not support HTML5 \n"
" video."
msgstr ""
msgstr "抱歉,由於您的瀏覽器不支援 HTML5 影片,本影片無法播放"
#: mediagoblin/templates/mediagoblin/media_displays/video.html:47
msgid ""
"You can get a modern web browser that \n"
" can play this video at <a href=\"http://getfirefox.com\">\n"
" http://getfirefox.com</a>!"
msgstr ""
msgstr "您可以在 <a href=\"http://getfirefox.com\">http://getfirefox.com</a> 取得可以播放此影片的先進瀏覽器。"
#: mediagoblin/templates/mediagoblin/media_displays/video.html:69
msgid "WebM file (640p; VP8/Vorbis)"
@ -880,19 +882,19 @@ msgstr "移除"
#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:21
#, python-format
msgid "%(username)s's collections"
msgstr ""
msgstr "%(username)s 的蒐藏"
#: mediagoblin/templates/mediagoblin/user_pages/collection_list.html:28
#, python-format
msgid "<a href=\"%(user_url)s\">%(username)s</a>'s collections"
msgstr ""
msgstr "<a href=\"%(user_url)s\">%(username)s</a> 的蒐藏"
#: mediagoblin/templates/mediagoblin/user_pages/comment_email.txt:19
#, python-format
msgid ""
"Hi %(username)s,\n"
"%(comment_author)s commented on your post (%(comment_url)s) at %(instance_name)s\n"
msgstr "%(username)s 您好:\n%(comment_author)s 在 %(instance_name)s 對您的內容 (%(comment_url)s) 張貼評論\n"
msgstr "%(username)s 您好:\n%(comment_author)s 在 %(instance_name)s 對您的內容 (%(comment_url)s) 張貼留言\n"
#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:30
#, python-format
@ -904,7 +906,7 @@ msgstr "%(username)s的媒體"
msgid ""
"<a href=\"%(user_url)s\">%(username)s</a>'s media with tag <a "
"href=\"%(tag_url)s\">%(tag)s</a>"
msgstr ""
msgstr "標籤為 <a href=\"%(tag_url)s\">%(tag)s</a> 的 <a href=\"%(user_url)s\">%(username)s</a> 的媒體"
#: mediagoblin/templates/mediagoblin/user_pages/gallery.html:48
#, python-format
@ -918,32 +920,32 @@ msgstr "❖ 瀏覽 <a href=\"%(user_url)s\">%(username)s</a> 的媒體"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:95
msgid "Add a comment"
msgstr "新增評論"
msgstr "新增留言"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:104
msgid "Add this comment"
msgstr "增加評論"
msgstr "增加留言"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:132
#: mediagoblin/templates/mediagoblin/user_pages/media.html:152
#: mediagoblin/templates/mediagoblin/user_pages/media.html:164
#, python-format
msgid "%(formatted_time)s ago"
msgstr ""
msgstr "%(formatted_time)s 前"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:150
msgid "Added"
msgstr ""
msgstr "新增於"
#: mediagoblin/templates/mediagoblin/user_pages/media.html:161
msgid "Created"
msgstr ""
msgstr "建立於"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:28
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:40
#, python-format
msgid "Add “%(media_title)s” to a collection"
msgstr ""
msgstr "加入 “%(media_title)s” 至蒐藏"
#: mediagoblin/templates/mediagoblin/user_pages/media_collect.html:54
msgid "+"
@ -1022,7 +1024,7 @@ msgstr "這個使用者(還)沒有填寫個人檔案。"
#: mediagoblin/templates/mediagoblin/user_pages/user.html:124
msgid "Browse collections"
msgstr ""
msgstr "瀏覽蒐藏"
#: mediagoblin/templates/mediagoblin/user_pages/user.html:137
#, python-format
@ -1047,11 +1049,11 @@ msgstr " (移除)"
#: mediagoblin/templates/mediagoblin/utils/collections.html:21
msgid "Collected in"
msgstr ""
msgstr "蒐集了"
#: mediagoblin/templates/mediagoblin/utils/collections.html:40
msgid "Add to a collection"
msgstr ""
msgstr "加入至蒐藏"
#: mediagoblin/templates/mediagoblin/utils/feed_link.html:21
#: mediagoblin/themes/airy/templates/mediagoblin/utils/feed_link.html:21
@ -1125,31 +1127,31 @@ msgstr "不好意思,看起來這個網址上沒有網頁。</p><p>如果您
#: mediagoblin/tools/timesince.py:62
msgid "year"
msgstr ""
msgstr ""
#: mediagoblin/tools/timesince.py:63
msgid "month"
msgstr ""
msgstr ""
#: mediagoblin/tools/timesince.py:64
msgid "week"
msgstr ""
msgstr ""
#: mediagoblin/tools/timesince.py:65
msgid "day"
msgstr ""
msgstr ""
#: mediagoblin/tools/timesince.py:66
msgid "hour"
msgstr ""
msgstr "小時"
#: mediagoblin/tools/timesince.py:67
msgid "minute"
msgstr ""
msgstr ""
#: mediagoblin/user_pages/forms.py:23
msgid "Comment"
msgstr ""
msgstr "留言"
#: mediagoblin/user_pages/forms.py:25
msgid ""
@ -1168,7 +1170,7 @@ msgstr "我確定我要從蒐藏中移除此項目"
#: mediagoblin/user_pages/forms.py:39
msgid "Collection"
msgstr ""
msgstr "蒐藏"
#: mediagoblin/user_pages/forms.py:40
msgid "-- Select --"
@ -1180,11 +1182,11 @@ msgstr "加註"
#: mediagoblin/user_pages/lib.py:58
msgid "commented on your post"
msgstr "在您的內容張貼評論"
msgstr "在您的內容張貼留言"
#: mediagoblin/user_pages/views.py:169
msgid "Sorry, comments are disabled."
msgstr ""
msgstr "抱歉,留言被關閉。"
#: mediagoblin/user_pages/views.py:174
msgid "Oops, your comment was empty."

View File

@ -58,16 +58,20 @@ def setup_global_and_app_config(config_path):
return global_config, app_config
def setup_database():
def setup_database(run_migrations=False):
app_config = mg_globals.app_config
global_config = mg_globals.global_config
# Load all models for media types (plugins, ...)
load_models(app_config)
# Set up the database
db = setup_connection_and_db_from_config(app_config)
check_db_migrations_current(db)
db = setup_connection_and_db_from_config(app_config, run_migrations)
if run_migrations:
#Run the migrations to initialize/update the database.
from mediagoblin.gmg_commands.dbupdate import run_all_migrations
run_all_migrations(db, app_config, global_config)
else:
check_db_migrations_current(db)
setup_globals(database=db)

View File

@ -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']))

View File

@ -14,6 +14,7 @@
# 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 import mg_globals
from mediagoblin.db.models import MediaEntry
from mediagoblin.db.util import media_entries_for_tag_slug
from mediagoblin.tools.pagination import Pagination
@ -80,6 +81,17 @@ def atom_feed(request):
link = request.urlgen('index', qualified=True)
feed_title += "for all recent items"
atomlinks = [
{'href': link,
'rel': 'alternate',
'type': 'text/html'}]
if mg_globals.app_config["push_urls"]:
for push_url in mg_globals.app_config["push_urls"]:
atomlinks.append({
'rel': 'hub',
'href': push_url})
cursor = cursor.order_by(MediaEntry.created.desc())
cursor = cursor.limit(ATOM_DEFAULT_NR_OF_UPDATED_ITEMS)
@ -87,9 +99,8 @@ def atom_feed(request):
feed_title,
feed_url=request.url,
id=link,
links=[{'href': link,
'rel': 'alternate',
'type': 'text/html'}])
links=atomlinks)
for entry in cursor:
feed.add(entry.get('title'),
entry.description_html,

View File

@ -111,7 +111,7 @@ class CsrfMeddleware(BaseMeddleware):
httponly=True)
# update the Vary header
response.vary = (getattr(response, 'vary', None) or []) + ['Cookie']
response.vary = list(getattr(response, 'vary', None) or []) + ['Cookie']
def _make_token(self, request):
"""Generate a new token to use for CSRF protection."""

View File

@ -15,12 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import logging
import tempfile
from mediagoblin import mg_globals
from mediagoblin.tools.common import import_component
from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
_log = logging.getLogger(__name__)
@ -52,36 +50,6 @@ class MediaManagerBase(object):
return hasattr(self, i)
class CompatMediaManager(object):
def __init__(self, mm_dict, entry=None):
self.mm_dict = mm_dict
self.entry = entry
def __call__(self, entry):
"So this object can look like a class too, somehow"
assert self.entry is None
return self.__class__(self.mm_dict, entry)
def __getitem__(self, i):
return self.mm_dict[i]
def __contains__(self, i):
return (i in self.mm_dict)
@property
def media_fetch_order(self):
return self.mm_dict.get('media_fetch_order')
def sniff_handler(self, *args, **kwargs):
func = self.mm_dict.get("sniff_handler", None)
if func is not None:
return func(*args, **kwargs)
return False
def __getattr__(self, i):
return self.mm_dict[i]
def sniff_media(media):
'''
Iterate through the enabled media types and find those suited
@ -98,40 +66,18 @@ def sniff_media(media):
media_file.write(media.stream.read())
media.stream.seek(0)
for media_type, manager in get_media_managers():
_log.info('Sniffing {0}'.format(media_type))
if manager.sniff_handler(media_file, media=media):
_log.info('{0} accepts the file'.format(media_type))
return media_type, manager
else:
_log.debug('{0} did not accept the file'.format(media_type))
media_type = hook_handle('sniff_handler', media_file, media=media)
if media_type:
_log.info('{0} accepts the file'.format(media_type))
return media_type, hook_handle(('media_manager', media_type))
else:
_log.debug('{0} did not accept the file'.format(media_type))
raise FileTypeNotSupported(
# TODO: Provide information on which file types are supported
_(u'Sorry, I don\'t support that file type :('))
def get_media_types():
"""
Generator, yields the available media types
"""
for media_type in mg_globals.app_config['media_types']:
yield media_type
def get_media_managers():
'''
Generator, yields all enabled media managers
'''
for media_type in get_media_types():
mm = import_component(media_type + ":MEDIA_MANAGER")
if isinstance(mm, dict):
mm = CompatMediaManager(mm)
yield media_type, mm
def get_media_type_and_manager(filename):
'''
Try to find the media type based on the file name, extension
@ -142,11 +88,10 @@ def get_media_type_and_manager(filename):
# Get the file extension
ext = os.path.splitext(filename)[1].lower()
for media_type, manager in get_media_managers():
# Omit the dot from the extension and match it against
# the media manager
if ext[1:] in manager.accepted_extensions:
return media_type, manager
# Omit the dot from the extension and match it against
# the media manager
if hook_handle('get_media_type_and_manager', ext[1:]):
return hook_handle('get_media_type_and_manager', ext[1:])
else:
_log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format(
filename))

View File

@ -17,15 +17,31 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.ascii.processing import process_ascii, \
sniff_handler
from mediagoblin.tools import pluginapi
ACCEPTED_EXTENSIONS = ["txt", "asc", "nfo"]
MEDIA_TYPE = 'mediagoblin.media_types.ascii'
def setup_plugin():
config = pluginapi.get_config(MEDIA_TYPE)
class ASCIIMediaManager(MediaManagerBase):
human_readable = "ASCII"
processor = staticmethod(process_ascii)
sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/ascii.html"
default_thumb = "images/media_thumbs/ascii.jpg"
accepted_extensions = ["txt", "asc", "nfo"]
MEDIA_MANAGER = ASCIIMediaManager
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:
return MEDIA_TYPE, ASCIIMediaManager
hooks = {
'setup': setup_plugin,
'get_media_type_and_manager': get_media_type_and_manager,
('media_manager', MEDIA_TYPE): lambda: ASCIIMediaManager,
'sniff_handler': sniff_handler,
}

View File

@ -28,17 +28,19 @@ from mediagoblin.media_types.ascii import asciitoimage
_log = logging.getLogger(__name__)
SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo']
MEDIA_TYPE = 'mediagoblin.media_types.ascii'
def sniff_handler(media_file, **kw):
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
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_EXTENSIONS:
return True
return MEDIA_TYPE
return False
return None
def process_ascii(proc_state):

View File

@ -17,14 +17,29 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.audio.processing import process_audio, \
sniff_handler
from mediagoblin.tools import pluginapi
ACCEPTED_EXTENSIONS = ["mp3", "flac", "wav", "m4a"]
MEDIA_TYPE = 'mediagoblin.media_types.audio'
def setup_plugin():
config = pluginapi.get_config(MEDIA_TYPE)
class AudioMediaManager(MediaManagerBase):
human_readable = "Audio"
processor = staticmethod(process_audio)
sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/audio.html"
accepted_extensions = ["mp3", "flac", "wav", "m4a"]
MEDIA_MANAGER = AudioMediaManager
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:
return MEDIA_TYPE, AudioMediaManager
hooks = {
'setup': setup_plugin,
'get_media_type_and_manager': get_media_type_and_manager,
'sniff_handler': sniff_handler,
('media_manager', MEDIA_TYPE): lambda: AudioMediaManager,
}

View File

@ -27,19 +27,22 @@ from mediagoblin.media_types.audio.transcoders import (AudioTranscoder,
_log = logging.getLogger(__name__)
MEDIA_TYPE = 'mediagoblin.media_types.audio'
def sniff_handler(media_file, **kw):
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
try:
transcoder = AudioTranscoder()
data = transcoder.discover(media_file.name)
except BadMediaFail:
_log.debug('Audio discovery raised BadMediaFail')
return False
return None
if data.is_audio == True and data.is_video == False:
return True
return MEDIA_TYPE
return False
return None
def process_audio(proc_state):

View File

@ -13,23 +13,30 @@
#
# 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 datetime
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.image.processing import process_image, \
sniff_handler
from mediagoblin.tools import pluginapi
ACCEPTED_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "tiff"]
MEDIA_TYPE = 'mediagoblin.media_types.image'
def setup_plugin():
config = pluginapi.get_config('mediagoblin.media_types.image')
class ImageMediaManager(MediaManagerBase):
human_readable = "Image"
processor = staticmethod(process_image)
sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/image.html"
default_thumb = "images/media_thumbs/image.png"
accepted_extensions = ["jpg", "jpeg", "png", "gif", "tiff"]
media_fetch_order = [u'medium', u'original', u'thumb']
def get_original_date(self):
"""
Get the original date and time from the EXIF information. Returns
@ -52,4 +59,14 @@ class ImageMediaManager(MediaManagerBase):
return None
MEDIA_MANAGER = ImageMediaManager
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:
return MEDIA_TYPE, ImageMediaManager
hooks = {
'setup': setup_plugin,
'get_media_type_and_manager': get_media_type_and_manager,
'sniff_handler': sniff_handler,
('media_manager', MEDIA_TYPE): lambda: ImageMediaManager,
}

View File

@ -35,6 +35,8 @@ PIL_FILTERS = {
'BICUBIC': Image.BICUBIC,
'ANTIALIAS': Image.ANTIALIAS}
MEDIA_TYPE = 'mediagoblin.media_types.image'
def resize_image(proc_state, resized, keyname, target_name, new_size,
exif_tags, workdir):
@ -95,17 +97,18 @@ def resize_tool(proc_state, force, keyname, target_name,
exif_tags, conversions_subdir)
SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg']
SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg', 'tiff']
def sniff_handler(media_file, **kw):
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
if kw.get('media') is not None: # That's a double negative!
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase
if clean_ext in SUPPORTED_FILETYPES:
_log.info('Found file extension in supported filetypes')
return True
return MEDIA_TYPE
else:
_log.debug('Media present, extension not found in {0}'.format(
SUPPORTED_FILETYPES))
@ -113,7 +116,7 @@ def sniff_handler(media_file, **kw):
_log.warning('Need additional information (keyword argument \'media\')'
' to be able to handle sniffing')
return False
return None
def process_image(proc_state):

View File

@ -17,15 +17,31 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.pdf.processing import process_pdf, \
sniff_handler
from mediagoblin.tools import pluginapi
ACCEPTED_EXTENSIONS = ['pdf']
MEDIA_TYPE = 'mediagoblin.media_types.pdf'
def setup_plugin():
config = pluginapi.get_config(MEDIA_TYPE)
class PDFMediaManager(MediaManagerBase):
human_readable = "PDF"
processor = staticmethod(process_pdf)
sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/pdf.html"
default_thumb = "images/media_thumbs/pdf.jpg"
accepted_extensions = ["pdf"]
MEDIA_MANAGER = PDFMediaManager
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:
return MEDIA_TYPE, PDFMediaManager
hooks = {
'setup': setup_plugin,
'get_media_type_and_manager': get_media_type_and_manager,
'sniff_handler': sniff_handler,
('media_manager', MEDIA_TYPE): lambda: PDFMediaManager,
}

View File

@ -25,6 +25,8 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _
_log = logging.getLogger(__name__)
MEDIA_TYPE = 'mediagoblin.media_types.pdf'
# TODO - cache (memoize) util
# This is a list created via uniconv --show and hand removing some types that
@ -163,16 +165,17 @@ def check_prerequisites():
return True
def sniff_handler(media_file, **kw):
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
if not check_prerequisites():
return False
return None
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_extensions():
return True
return MEDIA_TYPE
return False
return None
def create_pdf_thumb(original, thumb_filename, width, height):
# Note: pdftocairo adds '.png', remove it
@ -250,8 +253,8 @@ def process_pdf(proc_state):
else:
pdf_filename = queued_filename.rsplit('.', 1)[0] + '.pdf'
unoconv = where('unoconv')
call(executable=unoconv,
args=[unoconv, '-v', '-f', 'pdf', queued_filename])
Popen(executable=unoconv,
args=[unoconv, '-v', '-f', 'pdf', queued_filename]).wait()
if not os.path.exists(pdf_filename):
_log.debug('unoconv failed to convert file to pdf')
raise BadMediaFail()

View File

@ -17,15 +17,30 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.stl.processing import process_stl, \
sniff_handler
from mediagoblin.tools import pluginapi
MEDIA_TYPE = 'mediagoblin.media_types.stl'
ACCEPTED_EXTENSIONS = ["obj", "stl"]
def setup_plugin():
config = pluginapi.get_config(MEDIA_TYPE)
class STLMediaManager(MediaManagerBase):
human_readable = "stereo lithographics"
processor = staticmethod(process_stl)
sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/stl.html"
default_thumb = "images/media_thumbs/video.jpg"
accepted_extensions = ["obj", "stl"]
MEDIA_MANAGER = STLMediaManager
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:
return MEDIA_TYPE, STLMediaManager
hooks = {
'setup': setup_plugin,
'get_media_type_and_manager': get_media_type_and_manager,
'sniff_handler': sniff_handler,
('media_manager', MEDIA_TYPE): lambda: STLMediaManager,
}

View File

@ -29,6 +29,7 @@ from mediagoblin.media_types.stl import model_loader
_log = logging.getLogger(__name__)
SUPPORTED_FILETYPES = ['stl', 'obj']
MEDIA_TYPE = 'mediagoblin.media_types.stl'
BLEND_FILE = pkg_resources.resource_filename(
'mediagoblin.media_types.stl',
@ -43,13 +44,14 @@ BLEND_SCRIPT = pkg_resources.resource_filename(
def sniff_handler(media_file, **kw):
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
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
return MEDIA_TYPE
else:
_log.debug('Media present, extension not found in {0}'.format(
SUPPORTED_FILETYPES))
@ -57,7 +59,7 @@ def sniff_handler(media_file, **kw):
_log.warning('Need additional information (keyword argument \'media\')'
' to be able to handle sniffing')
return False
return None
def blender_render(config):

View File

@ -0,0 +1,27 @@
# 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 import mg_globals
_log = logging.getLogger(__name__)
def media_type_warning():
if mg_globals.app_config.get('media_types'):
_log.warning('Media_types have been converted to plugins. Old'
' media_types will no longer work. Please convert them'
' to plugins to continue using them.')

View File

@ -17,20 +17,35 @@
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.video.processing import process_video, \
sniff_handler
from mediagoblin.tools import pluginapi
MEDIA_TYPE = 'mediagoblin.media_types.video'
ACCEPTED_EXTENSIONS = [
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]
def setup_plugin():
config = pluginapi.get_config(MEDIA_TYPE)
class VideoMediaManager(MediaManagerBase):
human_readable = "Video"
processor = staticmethod(process_video)
sniff_handler = staticmethod(sniff_handler)
display_template = "mediagoblin/media_displays/video.html"
default_thumb = "images/media_thumbs/video.jpg"
accepted_extensions = [
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]
# Used by the media_entry.get_display_media method
media_fetch_order = [u'webm_640', u'original']
default_webm_type = 'video/webm; codecs="vp8, vorbis"'
MEDIA_MANAGER = VideoMediaManager
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:
return MEDIA_TYPE, VideoMediaManager
hooks = {
'setup': setup_plugin,
'get_media_type_and_manager': get_media_type_and_manager,
'sniff_handler': sniff_handler,
('media_manager', MEDIA_TYPE): lambda: VideoMediaManager,
}

View File

@ -29,6 +29,8 @@ from .util import skip_transcode
_log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG)
MEDIA_TYPE = 'mediagoblin.media_types.video'
class VideoTranscodingFail(BaseProcessingFail):
'''
@ -41,17 +43,18 @@ def sniff_handler(media_file, **kw):
transcoder = transcoders.VideoTranscoder()
data = transcoder.discover(media_file.name)
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
_log.debug('Discovered: {0}'.format(data))
if not data:
_log.error('Could not discover {0}'.format(
kw.get('media')))
return False
return None
if data['is_video'] == True:
return True
return MEDIA_TYPE
return False
return None
def process_video(proc_state):
@ -186,7 +189,7 @@ def store_metadata(media_entry, metadata):
[(key, tags_metadata[key])
for key in [
"application-name", "artist", "audio-codec", "bitrate",
"container-format", "copyright", "encoder",
"container-format", "copyright", "encoder",
"encoder-version", "license", "nominal-bitrate", "title",
"video-codec"]
if key in tags_metadata])
@ -203,7 +206,7 @@ def store_metadata(media_entry, metadata):
dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(),
dt.get_minute(), dt.get_second(),
dt.get_microsecond()).isoformat()
metadata['tags'] = tags
# Only save this field if there's something to save

View File

@ -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

View 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

View 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')

View 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]

View 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}

View 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))

View File

@ -0,0 +1,88 @@
# 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.plugins.basic_auth import forms as auth_forms
from mediagoblin.plugins.basic_auth import tools as auth_tools
from mediagoblin.auth.tools import create_basic_user
from mediagoblin.db.models import User
from mediagoblin.tools import pluginapi
from sqlalchemy import or_
def setup_plugin():
config = pluginapi.get_config('mediagoblin.plugins.basic_auth')
def get_user(**kwargs):
username = kwargs.pop('username', None)
if username:
user = User.query.filter(
or_(
User.username == username,
User.email == username,
)).first()
return user
def create_user(registration_form):
user = get_user(username=registration_form.username.data)
if not user and 'password' in registration_form:
user = create_basic_user(registration_form)
user.pw_hash = gen_password_hash(
registration_form.password.data)
user.save()
return user
def get_login_form(request):
return auth_forms.LoginForm(request.form)
def get_registration_form(request):
return auth_forms.RegistrationForm(request.form)
def gen_password_hash(raw_pass, extra_salt=None):
return auth_tools.bcrypt_gen_password_hash(raw_pass, extra_salt)
def check_password(raw_pass, stored_hash, extra_salt=None):
if stored_hash:
return auth_tools.bcrypt_check_password(raw_pass,
stored_hash, extra_salt)
return None
def auth():
return True
def append_to_global_context(context):
context['pass_auth'] = True
return context
hooks = {
'setup': setup_plugin,
'authentication': auth,
'auth_get_user': get_user,
'auth_create_user': create_user,
'auth_get_login_form': get_login_form,
'auth_get_registration_form': get_registration_form,
'auth_gen_password_hash': gen_password_hash,
'auth_check_password': check_password,
'auth_fake_login_attempt': auth_tools.fake_login_attempt,
'template_global_context': append_to_global_context,
}

View 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 wtforms
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.auth.tools import normalize_user_or_email_field
class RegistrationForm(wtforms.Form):
username = wtforms.TextField(
_('Username'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_email=False)])
password = wtforms.PasswordField(
_('Password'),
[wtforms.validators.Required(),
wtforms.validators.Length(min=5, max=1024)])
email = wtforms.TextField(
_('Email address'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_user=False)])
class LoginForm(wtforms.Form):
username = wtforms.TextField(
_('Username or Email'),
[wtforms.validators.Required(),
normalize_user_or_email_field()])
password = wtforms.PasswordField(
_('Password'))
stay_logged_in = wtforms.BooleanField(
label='',
description=_('Stay logged in'))

View File

@ -13,14 +13,8 @@
#
# 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 random
import bcrypt
from mediagoblin.tools.mail import send_email
from mediagoblin.tools.template import render_template
from mediagoblin import mg_globals
import random
def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None):
@ -88,33 +82,3 @@ def fake_login_attempt():
randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt)
randplus_stored_hash == randplus_hashed_pass
EMAIL_FP_VERIFICATION_TEMPLATE = (
u"http://{host}{uri}?"
u"userid={userid}&token={fp_verification_key}")
def send_fp_verification_email(user, request):
"""
Send the verification email to users to change their password.
Args:
- user: a user object
- request: the request
"""
rendered_email = render_template(
request, 'mediagoblin/auth/fp_verification_email.txt',
{'username': user.username,
'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format(
host=request.host,
uri=request.urlgen('mediagoblin.auth.verify_forgot_password'),
userid=unicode(user.id),
fp_verification_key=user.fp_verification_key)})
# TODO: There is no error handling in place
send_email(
mg_globals.app_config['email_sender_address'],
[user.email],
'GNU MediaGoblin - Change forgotten password!',
rendered_email)

View File

@ -0,0 +1,123 @@
# 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 os
import uuid
from sqlalchemy import or_
from mediagoblin.auth.tools import create_basic_user
from mediagoblin.db.models import User
from mediagoblin.plugins.openid.models import OpenIDUserURL
from mediagoblin.tools import pluginapi
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
PLUGIN_DIR = os.path.dirname(__file__)
def setup_plugin():
config = pluginapi.get_config('mediagoblin.plugins.openid')
routes = [
('mediagoblin.plugins.openid.register',
'/auth/openid/register/',
'mediagoblin.plugins.openid.views:register'),
('mediagoblin.plugins.openid.login',
'/auth/openid/login/',
'mediagoblin.plugins.openid.views:login'),
('mediagoblin.plugins.openid.finish_login',
'/auth/openid/login/finish/',
'mediagoblin.plugins.openid.views:finish_login'),
('mediagoblin.plugins.openid.edit',
'/edit/openid/',
'mediagoblin.plugins.openid.views:start_edit'),
('mediagoblin.plugins.openid.finish_edit',
'/edit/openid/finish/',
'mediagoblin.plugins.openid.views:finish_edit'),
('mediagoblin.plugins.openid.delete',
'/edit/openid/delete/',
'mediagoblin.plugins.openid.views:delete_openid'),
('mediagoblin.plugins.openid.finish_delete',
'/edit/openid/delete/finish/',
'mediagoblin.plugins.openid.views:finish_delete')]
pluginapi.register_routes(routes)
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
pluginapi.register_template_hooks(
{'register_link': 'mediagoblin/plugins/openid/register_link.html',
'login_link': 'mediagoblin/plugins/openid/login_link.html',
'edit_link': 'mediagoblin/plugins/openid/edit_link.html'})
def create_user(register_form):
if 'openid' in register_form:
username = register_form.username.data
user = User.query.filter(
or_(
User.username == username,
User.email == username,
)).first()
if not user:
user = create_basic_user(register_form)
new_entry = OpenIDUserURL()
new_entry.openid_url = register_form.openid.data
new_entry.user_id = user.id
new_entry.save()
return user
def extra_validation(register_form):
openid = register_form.openid.data if 'openid' in \
register_form else None
if openid:
openid_url_exists = OpenIDUserURL.query.filter_by(
openid_url=openid
).count()
extra_validation_passes = True
if openid_url_exists:
register_form.openid.errors.append(
_('Sorry, an account is already registered to that OpenID.'))
extra_validation_passes = False
return extra_validation_passes
def no_pass_redirect():
return 'openid'
def add_to_form_context(context):
context['openid_link'] = True
return context
def Auth():
return True
hooks = {
'setup': setup_plugin,
'authentication': Auth,
'auth_extra_validation': extra_validation,
'auth_create_user': create_user,
'auth_no_pass_redirect': no_pass_redirect,
('mediagoblin.auth.register',
'mediagoblin/auth/register.html'): add_to_form_context,
}

View File

@ -0,0 +1,41 @@
# 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 wtforms
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.auth.tools import normalize_user_or_email_field
class RegistrationForm(wtforms.Form):
openid = wtforms.HiddenField(
'',
[wtforms.validators.Required()])
username = wtforms.TextField(
_('Username'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_email=False)])
email = wtforms.TextField(
_('Email address'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_user=False)])
class LoginForm(wtforms.Form):
openid = wtforms.TextField(
_('OpenID'),
[wtforms.validators.Required(),
# Can openid's only be urls?
wtforms.validators.URL(message='Please enter a valid url.')])

View File

@ -0,0 +1,65 @@
# 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 sqlalchemy import Column, Integer, Unicode, ForeignKey
from sqlalchemy.orm import relationship, backref
from mediagoblin.db.models import User
from mediagoblin.db.base import Base
class OpenIDUserURL(Base):
__tablename__ = "openid__user_urls"
id = Column(Integer, primary_key=True)
openid_url = Column(Unicode, nullable=False)
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
# OpenID's are owned by their user, so do the full thing.
user = relationship(User, backref=backref('openid_urls',
cascade='all, delete-orphan'))
# OpenID Store Models
class Nonce(Base):
__tablename__ = "openid__nonce"
server_url = Column(Unicode, primary_key=True)
timestamp = Column(Integer, primary_key=True)
salt = Column(Unicode, primary_key=True)
def __unicode__(self):
return u'Nonce: %r, %r' % (self.server_url, self.salt)
class Association(Base):
__tablename__ = "openid__association"
server_url = Column(Unicode, primary_key=True)
handle = Column(Unicode, primary_key=True)
secret = Column(Unicode)
issued = Column(Integer)
lifetime = Column(Integer)
assoc_type = Column(Unicode)
def __unicode__(self):
return u'Association: %r, %r' % (self.server_url, self.handle)
MODELS = [
OpenIDUserURL,
Nonce,
Association
]

View File

@ -0,0 +1,127 @@
# 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 base64
import time
from openid.association import Association as OIDAssociation
from openid.store.interface import OpenIDStore
from openid.store import nonce
from mediagoblin.plugins.openid.models import Association, Nonce
class SQLAlchemyOpenIDStore(OpenIDStore):
def __init__(self):
self.max_nonce_age = 6 * 60 * 60
def storeAssociation(self, server_url, association):
assoc = Association.query.filter_by(
server_url=server_url, handle=association.handle
).first()
if not assoc:
assoc = Association()
assoc.server_url = unicode(server_url)
assoc.handle = association.handle
# django uses base64 encoding, python-openid uses a blob field for
# secret
assoc.secret = unicode(base64.encodestring(association.secret))
assoc.issued = association.issued
assoc.lifetime = association.lifetime
assoc.assoc_type = association.assoc_type
assoc.save()
def getAssociation(self, server_url, handle=None):
assocs = []
if handle is not None:
assocs = Association.query.filter_by(
server_url=server_url, handle=handle
)
else:
assocs = Association.query.filter_by(
server_url=server_url
)
if assocs.count() == 0:
return None
else:
associations = []
for assoc in assocs:
association = OIDAssociation(
assoc.handle, base64.decodestring(assoc.secret),
assoc.issued, assoc.lifetime, assoc.assoc_type
)
if association.getExpiresIn() == 0:
assoc.delete()
else:
associations.append((association.issued, association))
if not associations:
return None
associations.sort()
return associations[-1][1]
def removeAssociation(self, server_url, handle):
assocs = Association.query.filter_by(
server_url=server_url, handle=handle
).first()
assoc_exists = True if assocs else False
for assoc in assocs:
assoc.delete()
return assoc_exists
def useNonce(self, server_url, timestamp, salt):
if abs(timestamp - time.time()) > nonce.SKEW:
return False
ononce = Nonce.query.filter_by(
server_url=server_url,
timestamp=timestamp,
salt=salt
).first()
if ononce:
return False
else:
ononce = Nonce()
ononce.server_url = server_url
ononce.timestamp = timestamp
ononce.salt = salt
ononce.save()
return True
def cleanupNonces(self, _now=None):
if _now is None:
_now = int(time.time())
expired = Nonce.query.filter(
Nonce.timestamp < (_now - nonce.SKEW)
)
count = expired.count()
for each in expired:
each.delete()
return count
def cleanupAssociations(self):
now = int(time.time())
assoc = Association.query.all()
count = 0
for each in assoc:
if (each.lifetime + each.issued) <= now:
each.delete()
count = count + 1
return count

View File

@ -0,0 +1,44 @@
{#
# 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/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block title -%}
{% trans %}Add an OpenID{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}"
method="POST" enctype="multipart/form-data">
{{ csrf_token }}
<div class="form_box">
<h1>{% trans %}Add an OpenID{% endtrans %}</h1>
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}">
{% trans %}Delete an OpenID{% endtrans %}
</a>
</p>
{{ wtforms_util.render_divs(form, True) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Add{% endtrans %}" class="button_form"/>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,43 @@
{#
# 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/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block title -%}
{% trans %}Delete an OpenID{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}"
method="POST" enctype="multipart/form-data">
{{ csrf_token }}
<div class="form_box">
<h1>{% trans %}Delete an OpenID{% endtrans %}</h1>
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}">
{% trans %}Add an OpenID{% endtrans %}
</a>
</p>
{{ wtforms_util.render_divs(form, True) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/>
</div>
</div>
</form>
{% endblock %}

View 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/>.
#}
{% block openid_edit_link %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}">
{% trans %}Edit your OpenID's{% endtrans %}
</a>
</p>
{% endblock %}

View File

@ -0,0 +1,65 @@
{#
# 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/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_head %}
<script type="text/javascript"
src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script>
{% endblock %}
{% block title -%}
{% trans %}Log in{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<form action="{{ post_url }}"
method="POST" enctype="multipart/form-data">
{{ csrf_token }}
<div class="form_box">
<h1>{% trans %}Log in{% endtrans %}</h1>
{% if login_failed %}
<div class="form_field_error">
{% trans %}Logging in failed!{% endtrans %}
</div>
{% endif %}
{% if allow_registration %}
<p>
{% trans %}Log in to create an account!{% endtrans %}
</p>
{% endif %}
{% if pass_auth is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}">
{%- trans %}Or login with a password!{% endtrans %}
</a>
</p>
{% endif %}
{{ wtforms_util.render_divs(login_form, True) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/>
</div>
{% if next %}
<input type="hidden" name="next" value="{{ next }}" class="button_form"
style="display: none;"/>
{% endif %}
</div>
</form>
{% endblock %}

View 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/>.
#}
{% block openid_login_link %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}">
{%- trans %}Or login with OpenID!{% endtrans %}
</a>
</p>
{% endblock %}

View File

@ -0,0 +1,27 @@
{#
# 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/>.
#}
{% block openid_register_link %}
{% if openid_link is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}">
{%- trans %}Or register with OpenID!{% endtrans %}
</a>
</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,24 @@
{#
# 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/>.
#}
{% extends "mediagoblin/base.html" %}
{% block mediagoblin_content %}
<div onload="document.getElementById('openid_message').submit()">
{{ html|safe }}
</div>
{% endblock %}

View File

@ -0,0 +1,404 @@
# 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 openid.consumer import consumer
from openid.consumer.discover import DiscoveryFailure
from openid.extensions.sreg import SRegRequest, SRegResponse
from mediagoblin import mg_globals, messages
from mediagoblin.db.models import User
from mediagoblin.decorators import (auth_enabled, allow_registration,
require_active_login)
from mediagoblin.tools.response import redirect, render_to_response
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.plugins.openid import forms as auth_forms
from mediagoblin.plugins.openid.models import OpenIDUserURL
from mediagoblin.plugins.openid.store import SQLAlchemyOpenIDStore
from mediagoblin.auth.tools import register_user
def _start_verification(request, form, return_to, sreg=True):
"""
Start OpenID Verification.
Returns False if verification fails, otherwise, will return either a
redirect or render_to_response object
"""
openid_url = form.openid.data
c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore())
# Try to discover provider
try:
auth_request = c.begin(openid_url)
except DiscoveryFailure:
# Discovery failed, return to login page
form.openid.errors.append(
_('Sorry, the OpenID server could not be found'))
return False
host = 'http://' + request.host
if sreg:
# Ask provider for email and nickname
auth_request.addExtension(SRegRequest(required=['email', 'nickname']))
# Do we even need this?
if auth_request is None:
form.openid.errors.append(
_('No OpenID service was found for %s' % openid_url))
elif auth_request.shouldSendRedirect():
# Begin the authentication process as a HTTP redirect
redirect_url = auth_request.redirectURL(
host, return_to)
return redirect(
request, location=redirect_url)
else:
# Send request as POST
form_html = auth_request.htmlMarkup(
host, host + return_to,
# Is this necessary?
form_tag_attrs={'id': 'openid_message'})
# Beware: this renders a template whose content is a form
# and some javascript to submit it upon page load. Non-JS
# users will have to click the form submit button to
# initiate OpenID authentication.
return render_to_response(
request,
'mediagoblin/plugins/openid/request_form.html',
{'html': form_html})
return False
def _finish_verification(request):
"""
Complete OpenID Verification Process.
If the verification failed, will return false, otherwise, will return
the response
"""
c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore())
# Check the response from the provider
response = c.complete(request.args, request.base_url)
if response.status == consumer.FAILURE:
messages.add_message(
request,
messages.WARNING,
_('Verification of %s failed: %s' %
(response.getDisplayIdentifier(), response.message)))
elif response.status == consumer.SUCCESS:
# Verification was successfull
return response
elif response.status == consumer.CANCEL:
# Verification canceled
messages.add_message(
request,
messages.WARNING,
_('Verification cancelled'))
return False
def _response_email(response):
""" Gets the email from the OpenID providers response"""
sreg_response = SRegResponse.fromSuccessResponse(response)
if sreg_response and 'email' in sreg_response:
return sreg_response.data['email']
return None
def _response_nickname(response):
""" Gets the nickname from the OpenID providers response"""
sreg_response = SRegResponse.fromSuccessResponse(response)
if sreg_response and 'nickname' in sreg_response:
return sreg_response.data['nickname']
return None
@auth_enabled
def login(request):
"""OpenID Login View"""
login_form = auth_forms.LoginForm(request.form)
allow_registration = mg_globals.app_config["allow_registration"]
# Can't store next in request.GET because of redirects to OpenID provider
# Store it in the session
next = request.GET.get('next')
request.session['next'] = next
login_failed = False
if request.method == 'POST' and login_form.validate():
return_to = request.urlgen(
'mediagoblin.plugins.openid.finish_login')
success = _start_verification(request, login_form, return_to)
if success:
return success
login_failed = True
return render_to_response(
request,
'mediagoblin/plugins/openid/login.html',
{'login_form': login_form,
'next': request.session.get('next'),
'login_failed': login_failed,
'post_url': request.urlgen('mediagoblin.plugins.openid.login'),
'allow_registration': allow_registration})
@auth_enabled
def finish_login(request):
"""Complete OpenID Login Process"""
response = _finish_verification(request)
if not response:
# Verification failed, redirect to login page.
return redirect(request, 'mediagoblin.plugins.openid.login')
# Verification was successfull
query = OpenIDUserURL.query.filter_by(
openid_url=response.identity_url,
).first()
user = query.user if query else None
if user:
# Set up login in session
request.session['user_id'] = unicode(user.id)
request.session.save()
if request.session.get('next'):
return redirect(request, location=request.session.pop('next'))
else:
return redirect(request, "index")
else:
# No user, need to register
if not mg_globals.app.auth:
messages.add_message(
request,
messages.WARNING,
_('Sorry, authentication is disabled on this instance.'))
return redirect(request, 'index')
# Get email and nickname from response
email = _response_email(response)
username = _response_nickname(response)
register_form = auth_forms.RegistrationForm(request.form,
openid=response.identity_url,
email=email,
username=username)
return render_to_response(
request,
'mediagoblin/auth/register.html',
{'register_form': register_form,
'post_url': request.urlgen('mediagoblin.plugins.openid.register')})
@allow_registration
@auth_enabled
def register(request):
"""OpenID Registration View"""
if request.method == 'GET':
# Need to connect to openid provider before registering a user to
# get the users openid url. If method is 'GET', then this page was
# acessed without logging in first.
return redirect(request, 'mediagoblin.plugins.openid.login')
register_form = auth_forms.RegistrationForm(request.form)
if register_form.validate():
user = register_user(request, register_form)
if user:
# redirect the user to their homepage... there will be a
# message waiting for them to verify their email
return redirect(
request, 'mediagoblin.user_pages.user_home',
user=user.username)
return render_to_response(
request,
'mediagoblin/auth/register.html',
{'register_form': register_form,
'post_url': request.urlgen('mediagoblin.plugins.openid.register')})
@require_active_login
def start_edit(request):
"""Starts the process of adding an openid url to a users account"""
form = auth_forms.LoginForm(request.form)
if request.method == 'POST' and form.validate():
query = OpenIDUserURL.query.filter_by(
openid_url=form.openid.data
).first()
user = query.user if query else None
if not user:
return_to = request.urlgen('mediagoblin.plugins.openid.finish_edit')
success = _start_verification(request, form, return_to, False)
if success:
return success
else:
form.openid.errors.append(
_('Sorry, an account is already registered to that OpenID.'))
return render_to_response(
request,
'mediagoblin/plugins/openid/add.html',
{'form': form,
'post_url': request.urlgen('mediagoblin.plugins.openid.edit')})
@require_active_login
def finish_edit(request):
"""Finishes the process of adding an openid url to a user"""
response = _finish_verification(request)
if not response:
# Verification failed, redirect to add openid page.
return redirect(request, 'mediagoblin.plugins.openid.edit')
# Verification was successfull
query = OpenIDUserURL.query.filter_by(
openid_url=response.identity_url,
).first()
user_exists = query.user if query else None
if user_exists:
# user exists with that openid url, redirect back to edit page
messages.add_message(
request,
messages.WARNING,
_('Sorry, an account is already registered to that OpenID.'))
return redirect(request, 'mediagoblin.plugins.openid.edit')
else:
# Save openid to user
user = User.query.filter_by(
id=request.session['user_id']
).first()
new_entry = OpenIDUserURL()
new_entry.openid_url = response.identity_url
new_entry.user_id = user.id
new_entry.save()
messages.add_message(
request,
messages.SUCCESS,
_('Your OpenID url was saved successfully.'))
return redirect(request, 'mediagoblin.edit.account')
@require_active_login
def delete_openid(request):
"""View to remove an openid from a users account"""
form = auth_forms.LoginForm(request.form)
if request.method == 'POST' and form.validate():
# Check if a user has this openid
query = OpenIDUserURL.query.filter_by(
openid_url=form.openid.data
)
user = query.first().user if query.first() else None
if user and user.id == int(request.session['user_id']):
count = len(user.openid_urls)
if not count > 1 and not user.pw_hash:
# Make sure the user has a pw or another OpenID
messages.add_message(
request,
messages.WARNING,
_("You can't delete your only OpenID URL unless you"
" have a password set"))
elif user:
# There is a user, but not the same user who is logged in
form.openid.errors.append(
_('That OpenID is not registered to this account.'))
if not form.errors and not request.session.get('messages'):
# Okay to continue with deleting openid
return_to = request.urlgen(
'mediagoblin.plugins.openid.finish_delete')
success = _start_verification(request, form, return_to, False)
if success:
return success
return render_to_response(
request,
'mediagoblin/plugins/openid/delete.html',
{'form': form,
'post_url': request.urlgen('mediagoblin.plugins.openid.delete')})
@require_active_login
def finish_delete(request):
"""Finishes the deletion of an OpenID from an user's account"""
response = _finish_verification(request)
if not response:
# Verification failed, redirect to delete openid page.
return redirect(request, 'mediagoblin.plugins.openid.delete')
query = OpenIDUserURL.query.filter_by(
openid_url=response.identity_url
)
user = query.first().user if query.first() else None
# Need to check this again because of generic openid urls such as google's
if user and user.id == int(request.session['user_id']):
count = len(user.openid_urls)
if count > 1 or user.pw_hash:
# User has more then one openid or also has a password.
query.first().delete()
messages.add_message(
request,
messages.SUCCESS,
_('OpenID was successfully removed.'))
return redirect(request, 'mediagoblin.edit.account')
elif not count > 1:
messages.add_message(
request,
messages.WARNING,
_("You can't delete your only OpenID URL unless you have a "
"password set"))
return redirect(request, 'mediagoblin.plugins.openid.delete')
else:
messages.add_message(
request,
messages.WARNING,
_('That OpenID is not registered to this account.'))
return redirect(request, 'mediagoblin.plugins.openid.delete')

View File

@ -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)

View File

@ -129,6 +129,7 @@ header {
.header_dropdown {
margin-bottom: 20px;
padding: 0px 10px 0px 10px;
}
.header_dropdown li {
@ -333,6 +334,10 @@ text-align: center;
width: 20px;
}
.boolean {
margin-bottom: 8px;
}
textarea#description, textarea#bio {
resize: vertical;
height: 100px;
@ -384,6 +389,12 @@ a.comment_whenlink:hover, a.report_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%;

Some files were not shown because too many files have changed in this diff Show More