Merge branch 'master' into merge-python3-port

Has some issues, will iteratively fix!

Conflicts:
	mediagoblin/gmg_commands/__init__.py
	mediagoblin/gmg_commands/deletemedia.py
	mediagoblin/gmg_commands/users.py
	mediagoblin/oauth/views.py
	mediagoblin/plugins/api/views.py
	mediagoblin/tests/test_api.py
	mediagoblin/tests/test_edit.py
	mediagoblin/tests/test_oauth1.py
	mediagoblin/tests/test_util.py
	mediagoblin/tools/mail.py
	mediagoblin/webfinger/views.py
	setup.py
This commit is contained in:
Christopher Allan Webber 2014-09-16 14:01:43 -05:00
commit f6bad0eb26
215 changed files with 50324 additions and 10613 deletions

3
.gitmodules vendored
View File

@ -7,3 +7,6 @@
[submodule "extlib/skeleton"]
path = extlib/skeleton
url = git://github.com/dhg/Skeleton.git
[submodule "extlib/sandyseventiesspeedboat"]
path = extlib/sandyseventiesspeedboat
url = https://github.com/jpope777/sandyseventiesspeedboat-mg.git

10
AUTHORS
View File

@ -15,11 +15,14 @@ Thank you!
* Aleksej Serdjukov
* Alon Levy
* Alex Camelio
* Amirouche Boubekki
* András Veres-Szentkirályi
* Asheesh Laroia
* Andrew Browning
* Bassam Kurdali
* Bernhard Keller
* Berker Peksag
* Beuc
* Boris Bobrov
* Brandon Invergo
* Brett Smith
@ -44,6 +47,7 @@ Thank you!
* Jef van Schendel
* Jeremy Pope
* Jessica Tallon
* Jiyda Mint Moussa
* Jim Campbell
* Joar Wandborg
* Jorge Araya Navarro
@ -55,12 +59,14 @@ Thank you!
* Laura Arjona
* Larisa Hoffenbecker
* Lenna Peterson
* Loïc Le Ninan
* Luke Slater
* Manuel Urbano Santos
* Marcel van der Boom
* Mark Holmquist
* Mats Sjöberg
* Matt Lee
* Matt Molyneaux
* Michele Azzolari
* Mike Linksvayer
* Natalie Foust-Pilcher
@ -71,16 +77,20 @@ Thank you!
* Praveen Kumar
* Rasmus Larsson
* Rodney Ewing
* Rodrigo Rodrigues da Silva
* Runar Petursson
* Sacha De'Angeli
* Sam Clegg
* Sam Kleinman
* Sam Tuke
* Sebastian Hugentobler
* Sebastian Spaeth
* Sergio Durigan Junior
* Shawn Khan
* Simon Fondrie-Teitler
* Stefano Zacchiroli
* sturm
* thallian
* Tiberiu C. Turbureanu
* Tran Thanh Bao
* Tryggvi Björgvinsson

View File

@ -1,19 +0,0 @@
Metadata-Version: 1.2
Name: mediagoblin
Version: 0.4.0.dev
Summary: UNKNOWN
Home-page: http://mediagoblin.org/
Author: Free Software Foundation and contributors
Author-email: cwebber@gnu.org
License: AGPLv3
Download-URL: http://mediagoblin.org/download/
Description:
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content

View File

@ -1,13 +1,13 @@
# Extraction from Python source files
[python: mediagoblin/**.py]
# Extraction from Genshi HTML and text templates
# Extraction from Jinja2 HTML and text templates
[jinja2: mediagoblin/**/templates/**.html]
# Extract jinja templates (html)
# Extract Jinja2 templates (html)
encoding = utf-8
extensions = jinja2.ext.autoescape, mediagoblin.tools.template.TemplateHookExtension
[jinja2: mediagoblin/templates/**.txt]
# Extract jinja templates (text)
# Extract Jinja2 templates (text)
encoding = utf-8
extensions = jinja2.ext.autoescape

View File

@ -147,7 +147,7 @@ AC_PROG_INSTALL
# Check for a supported database program
AC_PATH_PROG([SQLITE], [sqlite3])
AC_PATH_PROG([POSTGRES], [postgres])
AC_PATH_PROG([POSTGRES], [psql])
AS_IF([test "x$SQLITE" = x -a "x$POSTGRES" = "x"],
[AC_MSG_ERROR([SQLite or PostgreSQL is required])])

155
docs/source/api/media.rst Normal file
View File

@ -0,0 +1,155 @@
.. 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/>.
.. info:: Currently only image uploading is supported.
===============
Uploading Media
===============
To use any the APIs mentioned in this document you will required :doc:`oauth`
Uploading and posting an media requiest you to make two to three requests:
1) Uploads the data to the server
2) Post media to feed
3) Update media to have title, description, license, etc. (optional)
These steps could be condenced in the future however currently this is how the
pump.io API works. There is currently an issue open, if you would like to change
how this works please contribute upstream: https://github.com/e14n/pump.io/issues/657
----------------------
Upload Media to Server
----------------------
To upload media you should use the URI `/api/user/<username>/uploads`.
A POST request should be made to the media upload URI submitting at least two header:
* `Content-Type` - This being a valid mimetype for the media.
* `Content-Length` - size in bytes of the media.
The media data should be submitted as POST data to the image upload URI.
You will get back a JSON encoded response which will look similiar to::
{
"updated": "2014-01-11T09:45:48Z",
"links": {
"self": {
"href": "https://<server>/image/4wiBUV1HT8GRqseyvX8m-w"
}
},
"fullImage": {
"url": "https://<server>//uploads/<username>/2014/1/11/V3cBMw.jpg",
"width": 505,
"height": 600
},
"replies": {
"url": "https://<server>//api/image/4wiBUV1HT8GRqseyvX8m-w/replies"
},
"image": {
"url": "https://<server>/uploads/<username>/2014/1/11/V3cBMw_thumb.jpg",
"width": 269,
"height": 320
},
"author": {
"preferredUsername": "<username>",
"displayName": "<username>",
"links": {
"activity-outbox": {
"href": "https://<server>/api/user/<username>/feed"
},
"self": {
"href": "https://<server>/api/user/<username>/profile"
},
"activity-inbox": {
"href": "https://<server>/api/user/<username>/inbox"
}
},
"url": "https://<server>/<username>",
"updated": "2013-08-14T10:01:21Z",
"id": "acct:<username>@<server>",
"objectType": "person"
},
"url": "https://<server>/<username>/image/4wiBUV1HT8GRqseyvX8m-w",
"published": "2014-01-11T09:45:48Z",
"id": "https://<server>/api/image/4wiBUV1HT8GRqseyvX8m-w",
"objectType": "image"
}
The main things in this response is `fullImage` which contains `url` (the URL
of the original image - i.e. fullsize) and `image` which contains `url` (the URL
of a thumbnail version).
.. warning:: Media which have been uploaded but not submitted to a feed will
periodically be deleted.
--------------
Submit to feed
--------------
This is submitting the media to appear on the website. This will create an
object in your feed which will then appear on the GNU MediaGoblin website so the
user and others can view and interact with the media.
The URL you need to POST to is `/api/user/<username>/feed`
You first should do a post to the feed URI with some of the information you got
back from the above request (which uploaded the media). The request should look
something like::
{
"verb": "post",
"object": {
"id": "https://<server>/api/image/6_K9m-2NQFi37je845c83w",
"objectType": "image"
}
}
.. warning:: Any other data submitted **will** be ignored
-------------------
Submitting Metadata
-------------------
Finally if you wish to set a title, description and license you will need to do
and update request to the endpoint, the following attributes can be submitted:
+--------------+---------------------------------------+-------------------+
| Name | Description | Required/Optional |
+==============+=======================================+===================+
| displayName | This is the title for the media | Optional |
+--------------+---------------------------------------+-------------------+
| content | This is the description for the media | Optional |
+--------------+---------------------------------------+-------------------+
| license | This is the license to be used | Optional |
+--------------+---------------------------------------+-------------------+
.. note:: license attribute is mediagoblin specific, pump.io does not support this attribute
The update request should look something similiar to::
{
"verb": "update",
"object": {
"displayName": "My super awesome image!",
"content": "The awesome image I took while backpacking to modor",
"license": "creativecommons.org/licenses/by-sa/3.0/",
"id": "https://<server>/api/image/6_K9m-2NQFi37je845c83w",
"objectType": "image"
}
}
.. warning:: Any other data submitted **will** be ignored.

View File

@ -0,0 +1,65 @@
.. 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/>.
Pump.io supports a number of different interactions that can happen against
media. Theser are commenting, liking/favoriting and (re-)sharing. Currently
MediaGoblin supports just commenting although other interactions will come at
a later date.
--------------
How to comment
--------------
.. warning:: Commenting on a comment currently is NOT supported.
Commenting is done by posting a comment activity to the users feed. The
activity should look similiar to::
{
"verb": "post",
"object": {
"objectType": "comment",
"inReplyTo": <media>
}
}
This is where `<media>` is the media object you have got with from the server.
----------------
Getting comments
----------------
The media object you get back should have a `replies` section. This should
be an object which contains the number of replies and if there are any (i.e.
number of replies > 0) then `items` will include an array of every item::
{
"totalItems": 2,
"items: [
{
"id": 1,
"objectType": "comment",
"content": "I'm a comment ^_^",
"author": <author user object>
},
{
"id": 4,
"objectType": "comment",
"content": "Another comment! Blimey!",
"author": <author user object>
}
],
"url": "http://some.server/api/images/1/comments/"
}

View File

@ -78,6 +78,7 @@ This guide covers writing new GNU MediaGoblin plugins.
pluginwriter/database
pluginwriter/api
pluginwriter/tests
pluginwriter/hooks
pluginwriter/media_type_hooks
pluginwriter/authhooks
@ -96,6 +97,26 @@ This chapter contains various information for developers.
devel/migrations
Part 5: Pump API
================
This chapter covers MediaGoblin's `Pump API
<https://github.com/e14n/pump.io/blob/master/API.md>`_ support. (A
work in progress; full federation is not supported at the moment, but
media uploading works! You can use something like
`PyPump <http://pypump.org>`_
to write MediaGoblin uploadable applications.)
.. toctree::
:maxdepth: 1
api/client_register
api/oauth
api/media
api/media_interaction
Indices and tables
==================

View File

@ -0,0 +1,35 @@
.. MediaGoblin Documentation
Written in 2014 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/>.
===============================
Documentation on Built-in Hooks
===============================
This section explains built-in hooks to MediaGoblin.
What hooks are available?
=========================
'collection_add_media'
----------------------
This hook is used by ``add_media_to_collection``
in ``mediagoblin.user_pages.lib``.
It gets a ``CollectionItem`` as its argument.
It's the newly created item just before getting commited.
So the item can be modified by the hook, if needed.
Changing the session regarding this item is currently
undefined behaviour, as the SQL Session might contain other
things.

View File

@ -15,7 +15,13 @@
Command-line uploading
======================
Want to submit media via the command line? It's fairly easy to do::
If you're a site administrator and have access to the server then you
can use the 'addmedia' task. If you're just a user and want to upload
media by the command line you can. This can be done with the pump.io
API. There is `p <https://github.com/xray7224/p/>`_, which will allow you
to easily upload media from the command line, follow p's docs to do that.
To use the addmedia command::
./bin/gmg addmedia username your_media.jpg
@ -39,3 +45,70 @@ You can also pass in the `--celery` option if you would prefer that
your media be passed over to celery to be processed rather than be
processed immediately.
============================
Command-line batch uploading
============================
There's another way to submit media, and it can be much more powerful, although
it is a bit more complex.
./bin/gmg batchaddmedia admin /path/to/your/metadata.csv
This is an example of what a script may look like. The important part here is
that you have to create the 'metadata.csv' file.::
media:location,dcterms:title,dcterms:creator,dcterms:type
"http://www.example.net/path/to/nap.png","Goblin taking a nap",,"Image"
"http://www.example.net/path/to/snore.ogg","Goblin Snoring","Me","Audio"
The above is an example of a very simple metadata.csv file. The batchaddmedia
script would read this and attempt to upload only two pieces of media, and would
be able to automatically name them appropriately.
The csv file
============
The location column
-------------------
The location column is the one column that is absolutely necessary for
uploading your media. This gives a path to each piece of media you upload. This
can either a path to a local file or a direct link to remote media (with the
link in http format). As you can see in the example above the (fake) media was
stored remotely on "www.example.net".
Other internal nodes
--------------------
There are other columns which can be used by the script to provide information.
These are not stored as part of the media's metadata. You can use these columns to
provide default information for your media entry, but as you'll see below, it's
just as easy to provide this information through the correct metadata columns.
- **id** is used to identify the media entry to the user in case of an error in the batchaddmedia script.
- **license** is used to set a license for your piece a media for mediagoblin's use. This must be a URI.
- **title** will set the title displayed to mediagoblin users.
- **description** will set a description of your media.
Metadata columns
----------------
Other columns can be used to provide detailed metadata about each media entry.
Our metadata system accepts any information provided for in the
`RDFa Core Initial Context`_, and the batchupload script recognizes all of the
resources provided within it.
.. _RDFa Core Initial Context: http://www.w3.org/2011/rdfa-context/rdfa-1.1
The uploader may include the metadata for each piece of media, or
leave them blank if they want to. A few columns from `Dublin Core`_ are
notable because the batchaddmedia script also uses them to set the default
information of uploaded media entries.
.. _Dublin Core: http://wiki.dublincore.org/index.php/User_Guide
- **dc:title** sets a title for your media entry.
- **dc:description** sets a description of your media entry.
If both a metadata column and an internal node for the title are provided, mediagoblin
will use the internal node as the media entry's display name. This makes it so
that if you want to display a piece of media with a different title
than the one provided in its metadata, you can just provide different data for
the 'dc:title' and 'title' columns. The same is true of the 'description' and
'dc:description'.

View File

@ -165,11 +165,11 @@ to the unpriviledged system account.
To do this, enter either of the following commands, changing the defaults
to suit your particular requirements::
sudo mkdir -p /srv/mediagoblin.example.org && sudo chown -hR mediagoblin:mediagoblin /srv/mediagoblin.example.org
sudo mkdir -p /srv/mediagoblin.example.org && sudo chown -hR mediagoblin: /srv/mediagoblin.example.org
or (as the root user)::
mkdir -p /srv/mediagoblin.example.org && chown -hR mediagoblin:mediagoblin /srv/mediagoblin.example.org
mkdir -p /srv/mediagoblin.example.org && chown -hR mediagoblin: /srv/mediagoblin.example.org
Install MediaGoblin and Virtualenv
@ -200,7 +200,7 @@ Clone the MediaGoblin repository and set up the git submodules::
And set up the in-package virtualenv::
(virtualenv --system-site-packages . || virtualenv .) && ./bin/python setup.py develop
(virtualenv --python=python2 --system-site-packages . || virtualenv --python=python2 .) && ./bin/python setup.py develop
.. note::
@ -214,16 +214,6 @@ And set up the in-package virtualenv::
Note: this is liable to break. Use this method with caution.
.. ::
(NOTE: Is this still relevant?)
If you have problems here, consider trying to install virtualenv
with the ``--distribute`` or ``--no-site-packages`` options. If
your system's default Python is in the 3.x series you may need to
run ``virtualenv`` with the ``--python=python2.7`` or
``--python=python2.6`` options.
The above provides an in-package install of ``virtualenv``. While this
is counter to the conventional ``virtualenv`` configuration, it is
more reliable and considerably easier to configure and illustrate. If
@ -244,7 +234,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 && git submodule fetch
git submodule update && ./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate
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

@ -1,6 +1,6 @@
.. MediaGoblin Documentation
Written in 2011, 2012 by MediaGoblin contributors
Written in 2011, 2012, 2014 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
@ -18,8 +18,8 @@ Media Types
====================
In the future, there will be all sorts of media types you can enable,
but in the meanwhile there are five additional media types: video, audio,
ascii art, STL/3d models, PDF and Document.
but in the meanwhile there are six additional media types: video, audio,
raw image, 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.
@ -149,6 +149,28 @@ Run
You should now be able to upload and listen to audio files!
Raw image
=========
To enable raw image you need to install pyexiv2. On Debianoid systems
.. code-block:: bash
sudo apt-get install python-pyexiv2
Add ``[[mediagoblin.media_types.raw_image]]`` under the ``[plugins]``
section in your ``mediagoblin_local.ini`` and restart MediaGoblin.
Run
.. code-block:: bash
./bin/gmg dbupdate
Now you should be able to submit raw images, and mediagoblin should
extract the JPEG preview from them.
Ascii art
=========
@ -242,3 +264,13 @@ Run
./bin/gmg dbupdate
Blog (HIGHLY EXPERIMENTAL)
==========================
MediaGoblin has a blog media type, which you might notice by looking
through the docs! However, it is *highly experimental*. We have not
security reviewed this, and it acts in a way that is not like normal
blogs (the blogposts are themselves media types!).
So you can play with this, but it is not necessarily recommended yet
for production use! :)

View File

@ -21,6 +21,85 @@ 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.7.0
====
**Do this to upgrade**
1. Update to the latest release. If checked out from git, run:
``git fetch && git checkout -q v0.7.0 && git submodule init && git submodule update``
2. Make sure to run
``./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate``
(NOTE: earlier versions of the 0.7.0 release instructions left out the
``git submodule init`` step! If you did an upgrade earlier based on
these instructions and your theme looks weirdly aligned, try running
the following:)
``git submodule init && git submodule update``
That's it, probably! If you run into problems, don't hesitate to
`contact us <http://mediagoblin.org/pages/join.html>`_
(IRC is often best).
**New features:**
- New mobile upload API making use of the
`Pump API <https://github.com/e14n/pump.io/blob/master/API.md>`_
(which will be the foundation for MediaGoblin's federation)
- New theme: Sandy 70s Speedboat!
- Metadata features! We also now have a json-ld context.
- Many improvements for archival institutions, including metadata
support and featuring items on the homepage. With the (new!)
archivalook plugin enabled, featuring media is possible.
Additionally, metadata about the particular media item will show up
in the sidebar.
In the future these plugins may be separated, but for now they have
come together as part of the same plugin.
- There is a new gmg subcommand called batchaddmedia that allows for
uploading many files at once. This is aimed to be useful for
archival institutions and groups where there is an already existing
and large set of available media that needs to be included.
- Speaking of, the call to postgres in the makefile is fixed.
- We have a new, generic media-page context hook that allows for
adding context depending on the type of media.
- Tired of video thumbnails breaking during processing all the time?
Good news, everyone! Video thumbnail generation should not fail
frequently anymore. (We think...)
- You can now set default permissions for new users in the config.
- bootstrap.sh / gnu configuration stuff still exists, but moves to be
experimental-bootstrap.sh so as to not confuse newcomers. There are
some problems currently with the autoconf stuff that we need to work
out... we still have interest in supporting it, though help is
welcome.
- MediaGoblin now checks whether or not the database is up to date
when starting.
- Switched to `Skeleton <http://www.getskeleton.com/>`_ as a system for
graphic design.
- New gmg subcommands for administrators:
- A "deletemedia" command
- A "deleteuser" command
- We now have a blogging media type... it's very experimental,
however. Use with caution!
- We have switched to exifread as an external library for reading EXIF
data. It's basically the same thing as before, but packaged
separately from MediaGoblin.
- Many improvements to internationalization. Also (still rudimentary,
but existant!) RTL language support!
**Known issues:**
- The host-meta is now json by default; in the spec it should be xml by
default. We have done this because of compatibility with the pump
API. We are checking with upstream to see if there is a way to
resolve this discrepancy.
0.6.1
=====

@ -0,0 +1 @@
Subproject commit 8873d9b559a4c5b3bb90997227d5455f8730fd48

View File

@ -75,7 +75,7 @@ case "$selfname" in
lazycelery.sh)
MEDIAGOBLIN_CONFIG="${ini_file}" \
CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery \
$starter "$@"
$starter -B "$@"
;;
*) exit 1 ;;
esac

View File

@ -27,14 +27,15 @@ allow_reporting = true
# local_templates = %(here)s/user_dev/templates/
## You can set your theme by specifying this (not specifying it will
## use the default theme). Run `gmg theme assetlink` to apply the change.
## The airy theme comes with GMG; please see the theming docs on how to
## install other themes.
## use the default theme). Run `gmg assetlink` to apply the change.
## The airy and sandyseventiesspeedboat theme comes with GMG; please
## see the theming docs on how to install other themes.
# theme = airy
## If you want the terms of service displayed, you can uncomment this
# show_tos = true
user_privilege_scheme = "uploader,commenter,reporter"
[storage:queuestore]
base_dir = %(here)s/user_dev/media/queue

View File

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

View File

@ -234,6 +234,8 @@ class MediaGoblinApp(object):
request, e,
e.get_description(environ))(environ, start_response)
request = hook_transform("modify_request", request)
request.start_response = start_response
# get the Http response from the controller

View File

@ -25,7 +25,6 @@ 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

View File

@ -134,11 +134,7 @@ def register_user(request, register_form):
user = auth.create_user(register_form)
# give the user the default privileges
default_privileges = [
Privilege.query.filter(Privilege.privilege_name==u'commenter').first(),
Privilege.query.filter(Privilege.privilege_name==u'uploader').first(),
Privilege.query.filter(Privilege.privilege_name==u'reporter').first()]
user.all_privileges += default_privileges
user.all_privileges += get_default_privileges(user)
user.save()
# log the user in
@ -153,6 +149,14 @@ def register_user(request, register_form):
return None
def get_default_privileges(user):
instance_privilege_scheme = mg_globals.app_config['user_privilege_scheme']
default_privileges = [Privilege.query.filter(
Privilege.privilege_name==privilege_name).first()
for privilege_name in instance_privilege_scheme.split(',')]
default_privileges = [privilege for privilege in default_privileges if not privilege == None]
return default_privileges
def check_login_simple(username, password):
user = auth.get_user(username=username)

View File

@ -23,13 +23,29 @@ direct_remote_path = string(default="/mgoblin_static/")
# set to false to enable sending notices
email_debug_mode = boolean(default=True)
# Uses SSL/TLS when connecting to SMTP server
email_smtp_use_ssl = boolean(default=False)
# Uses STARTTLS when connecting to SMTP server
email_smtp_force_starttls = boolean(default=False)
# Email address which notices are sent from
email_sender_address = string(default="notice@mediagoblin.example.org")
# Hostname of SMTP server
email_smtp_host = string(default='')
# Port for SMTP server
email_smtp_port = integer(default=0)
# Username used for SMTP server
email_smtp_user = string(default=None)
# Password used for SMTP server
email_smtp_pass = string(default=None)
# Set to false to disable registrations
allow_registration = boolean(default=True)
@ -89,6 +105,13 @@ upload_limit = integer(default=None)
# Max file size (in Mb)
max_file_size = integer(default=None)
# Privilege scheme
user_privilege_scheme = string(default="uploader,commenter,reporter")
# Frequency garbage collection will run (setting to 0 or false to disable)
# Setting units are minutes.
garbage_collection = integer(default=60)
[jinja2]
# Jinja2 supports more directives than the minimum required by mediagoblin.
# This setting allows users creating custom templates to specify a list of

View File

@ -14,36 +14,3 @@
# 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/>.
"""
Database Abstraction/Wrapper Layer
==================================
This submodule is for most of the db specific stuff.
There are two main ideas here:
1. Open up a small possibility to replace mongo by another
db. This means, that all direct mongo accesses should
happen in the db submodule. While all the rest uses an
API defined by this submodule.
Currently this API happens to be basicly mongo.
Which means, that the abstraction/wrapper layer is
extremely thin.
2. Give the rest of the app a simple and easy way to get most of
their db needs. Which often means some simple import
from db.util.
What does that mean?
* Never import mongo directly outside of this submodule.
* Inside this submodule you can do whatever is needed. The
API border is exactly at the submodule layer. Nowhere
else.
* helper functions can be moved in here. They become part
of the db.* API
"""

View File

@ -21,18 +21,19 @@ import six
from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
Integer, Unicode, UnicodeText, DateTime,
ForeignKey, Date)
ForeignKey, Date, Index)
from sqlalchemy.exc import ProgrammingError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import and_
from sqlalchemy.schema import UniqueConstraint
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
from mediagoblin.db.migration_tools import (
RegisterMigration, inspect_table, replace_table_hack)
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
Privilege)
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
Privilege)
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
MIGRATIONS = {}
@ -467,7 +468,6 @@ def create_oauth1_tables(db):
db.commit()
@RegisterMigration(15, MIGRATIONS)
def wants_notifications(db):
"""Add a wants_notifications field to User model"""
@ -661,8 +661,8 @@ def create_moderation_tables(db):
# admin, an active user or an inactive user ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
for admin_user in admin_users_ids:
admin_user_id = admin_user['id']
for privilege_id in [admin_privilege_id, uploader_privilege_id,
reporter_privilege_id, commenter_privilege_id,
for privilege_id in [admin_privilege_id, uploader_privilege_id,
reporter_privilege_id, commenter_privilege_id,
active_privilege_id]:
db.execute(user_privilege_assoc.insert().values(
core__privilege_id=admin_user_id,
@ -670,7 +670,7 @@ def create_moderation_tables(db):
for active_user in active_users_ids:
active_user_id = active_user['id']
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
commenter_privilege_id, active_privilege_id]:
db.execute(user_privilege_assoc.insert().values(
core__privilege_id=active_user_id,
@ -678,7 +678,7 @@ def create_moderation_tables(db):
for inactive_user in inactive_users_ids:
inactive_user_id = inactive_user['id']
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
commenter_privilege_id]:
db.execute(user_privilege_assoc.insert().values(
core__privilege_id=inactive_user_id,
@ -709,6 +709,8 @@ def create_moderation_tables(db):
is_admin.drop()
db.commit()
@RegisterMigration(19, MIGRATIONS)
def drop_MediaEntry_collected(db):
"""
@ -722,3 +724,171 @@ def drop_MediaEntry_collected(db):
media_collected.drop()
db.commit()
@RegisterMigration(20, MIGRATIONS)
def add_metadata_column(db):
metadata = MetaData(bind=db.bind)
media_entry = inspect_table(metadata, 'core__media_entries')
col = Column('media_metadata', MutationDict.as_mutable(JSONEncoded),
default=MutationDict())
col.create(media_entry)
db.commit()
class PrivilegeUserAssociation_R1(declarative_base()):
__tablename__ = 'rename__privileges_users'
user = Column(
"user",
Integer,
ForeignKey(User.id),
primary_key=True)
privilege = Column(
"privilege",
Integer,
ForeignKey(Privilege.id),
primary_key=True)
@RegisterMigration(21, MIGRATIONS)
def fix_privilege_user_association_table(db):
"""
There was an error in the PrivilegeUserAssociation table that allowed for a
dangerous sql error. We need to the change the name of the columns to be
unique, and properly referenced.
"""
metadata = MetaData(bind=db.bind)
privilege_user_assoc = inspect_table(
metadata, 'core__privileges_users')
# This whole process is more complex if we're dealing with sqlite
if db.bind.url.drivername == 'sqlite':
PrivilegeUserAssociation_R1.__table__.create(db.bind)
db.commit()
new_privilege_user_assoc = inspect_table(
metadata, 'rename__privileges_users')
result = db.execute(privilege_user_assoc.select())
for row in result:
# The columns were improperly named before, so we switch the columns
user_id, priv_id = row['core__privilege_id'], row['core__user_id']
db.execute(new_privilege_user_assoc.insert().values(
user=user_id,
privilege=priv_id))
db.commit()
privilege_user_assoc.drop()
new_privilege_user_assoc.rename('core__privileges_users')
# much simpler if postgres though!
else:
privilege_user_assoc.c.core__user_id.alter(name="privilege")
privilege_user_assoc.c.core__privilege_id.alter(name="user")
db.commit()
@RegisterMigration(22, MIGRATIONS)
def add_index_username_field(db):
"""
This migration has been found to be doing the wrong thing. See
the documentation in migration 23 (revert_username_index) below
which undoes this for those databases that did run this migration.
Old description:
This indexes the User.username field which is frequently queried
for example a user logging in. This solves the issue #894
"""
## This code is left commented out *on purpose!*
##
## We do not normally allow commented out code like this in
## MediaGoblin but this is a special case: since this migration has
## been nullified but with great work to set things back below,
## this is commented out for historical clarity.
#
# metadata = MetaData(bind=db.bind)
# user_table = inspect_table(metadata, "core__users")
#
# new_index = Index("ix_core__users_uploader", user_table.c.username)
# new_index.create()
#
# db.commit()
pass
@RegisterMigration(23, MIGRATIONS)
def revert_username_index(db):
"""
Revert the stuff we did in migration 22 above.
There were a couple of problems with what we did:
- There was never a need for this migration! The unique
constraint had an implicit b-tree index, so it wasn't really
needed. (This is my (Chris Webber's) fault for suggesting it
needed to happen without knowing what's going on... my bad!)
- On top of that, databases created after the models.py was
changed weren't the same as those that had been run through
migration 22 above.
As such, we're setting things back to the way they were before,
but as it turns out, that's tricky to do!
"""
metadata = MetaData(bind=db.bind)
user_table = inspect_table(metadata, "core__users")
indexes = dict(
[(index.name, index) for index in user_table.indexes])
# index from unnecessary migration
users_uploader_index = indexes.get(u'ix_core__users_uploader')
# index created from models.py after (unique=True, index=True)
# was set in models.py
users_username_index = indexes.get(u'ix_core__users_username')
if users_uploader_index is None and users_username_index is None:
# We don't need to do anything.
# The database isn't in a state where it needs fixing
#
# (ie, either went through the previous borked migration or
# was initialized with a models.py where core__users was both
# unique=True and index=True)
return
if db.bind.url.drivername == 'sqlite':
# Again, sqlite has problems. So this is tricky.
# Yes, this is correct to use User_vR1! Nothing has changed
# between the *correct* version of this table and migration 18.
User_vR1.__table__.create(db.bind)
db.commit()
new_user_table = inspect_table(metadata, 'rename__users')
replace_table_hack(db, user_table, new_user_table)
else:
# If the db is not run using SQLite, we don't need to do crazy
# table copying.
# Remove whichever of the not-used indexes are in place
if users_uploader_index is not None:
users_uploader_index.drop()
if users_username_index is not None:
users_username_index.drop()
# Given we're removing indexes then adding a unique constraint
# which *we know might fail*, thus probably rolling back the
# session, let's commit here.
db.commit()
try:
# Add the unique constraint
constraint = UniqueConstraint(
'username', table=user_table)
constraint.create()
except ProgrammingError:
# constraint already exists, no need to add
db.rollback()
db.commit()

View File

@ -202,6 +202,17 @@ class MediaEntryMixin(GenerateSlugMixin):
thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
return thumb_url
@property
def original_url(self):
""" Returns the URL for the original image
will return self.thumb_url if original url doesn't exist"""
if u"original" not in self.media_files:
return self.thumb_url
return mg_globals.app.public_store.file_url(
self.media_files[u"original"]
)
@cached_property
def media_manager(self):
"""Returns the MEDIA_MANAGER of the media's media_type
@ -248,7 +259,7 @@ class MediaEntryMixin(GenerateSlugMixin):
if 'Image DateTimeOriginal' in exif_all:
# format date taken
takendate = datetime.datetime.strptime(
takendate = datetime.strptime(
exif_all['Image DateTimeOriginal']['printable'],
'%Y:%m:%d %H:%M:%S').date()
taken = takendate.strftime('%B %d %Y')
@ -294,6 +305,13 @@ class MediaCommentMixin(object):
"""
return cleaned_markdown_conversion(self.content)
def __unicode__(self):
return u'<{klass} #{id} {author} "{comment}">'.format(
klass=self.__class__.__name__,
id=self.id,
author=self.get_author,
comment=self.content)
def __repr__(self):
return '<{klass} #{id} {author} "{comment}">'.format(
klass=self.__class__.__name__,

View File

@ -101,25 +101,26 @@ class User(Base, UserMixin):
super(User, self).delete(**kwargs)
_log.info('Deleted user "{0}" account'.format(self.username))
def has_privilege(self,*priv_names):
def has_privilege(self, privilege, allow_admin=True):
"""
This method checks to make sure a user has all the correct privileges
to access a piece of content.
:param priv_names A variable number of unicode objects which rep-
-resent the different privileges which may give
the user access to this content. If you pass
multiple arguments, the user will be granted
access if they have ANY of the privileges
passed.
:param privilege A unicode object which represent the different
privileges which may give the user access to
content.
:param allow_admin If this is set to True the then if the user is
an admin, then this will always return True
even if the user hasn't been given the
privilege. (defaults to True)
"""
if len(priv_names) == 1:
priv = Privilege.query.filter(
Privilege.privilege_name==priv_names[0]).one()
return (priv in self.all_privileges)
elif len(priv_names) > 1:
return self.has_privilege(priv_names[0]) or \
self.has_privilege(*priv_names[1:])
priv = Privilege.query.filter_by(privilege_name=privilege).one()
if priv in self.all_privileges:
return True
elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
return True
return False
def is_banned(self):
@ -132,6 +133,48 @@ class User(Base, UserMixin):
return UserBan.query.get(self.id) is not None
def serialize(self, request):
user = {
"id": "acct:{0}@{1}".format(self.username, request.host),
"preferredUsername": self.username,
"displayName": "{0}@{1}".format(self.username, request.host),
"objectType": "person",
"pump_io": {
"shared": False,
"followed": False,
},
"links": {
"self": {
"href": request.urlgen(
"mediagoblin.federation.user.profile",
username=self.username,
qualified=True
),
},
"activity-inbox": {
"href": request.urlgen(
"mediagoblin.federation.inbox",
username=self.username,
qualified=True
)
},
"activity-outbox": {
"href": request.urlgen(
"mediagoblin.federation.feed",
username=self.username,
qualified=True
)
},
},
}
if self.bio:
user.update({"summary": self.bio})
if self.url:
user.update({"url": self.url})
return user
class Client(Base):
"""
Model representing a client - Used for API Auth
@ -197,7 +240,6 @@ class NonceTimestamp(Base):
nonce = Column(Unicode, nullable=False, primary_key=True)
timestamp = Column(DateTime, nullable=False, primary_key=True)
class MediaEntry(Base, MediaEntryMixin):
"""
TODO: Consider fetching the media_files using join
@ -260,6 +302,8 @@ class MediaEntry(Base, MediaEntryMixin):
cascade="all, delete-orphan"
)
collections = association_proxy("collections_helper", "in_collection")
media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
default=MutationDict())
## TODO
# fail_error
@ -382,6 +426,80 @@ class MediaEntry(Base, MediaEntryMixin):
# pass through commit=False/True in kwargs
super(MediaEntry, self).delete(**kwargs)
@property
def objectType(self):
""" Converts media_type to pump-like type - don't use internally """
return self.media_type.split(".")[-1]
def serialize(self, request, show_comments=True):
""" Unserialize MediaEntry to object """
author = self.get_uploader
context = {
"id": self.id,
"author": author.serialize(request),
"objectType": self.objectType,
"url": self.url_for_self(request.urlgen),
"image": {
"url": request.host_url + self.thumb_url[1:],
},
"fullImage":{
"url": request.host_url + self.original_url[1:],
},
"published": self.created.isoformat(),
"updated": self.created.isoformat(),
"pump_io": {
"shared": False,
},
"links": {
"self": {
"href": request.urlgen(
"mediagoblin.federation.object",
objectType=self.objectType,
id=self.id,
qualified=True
),
},
}
}
if self.title:
context["displayName"] = self.title
if self.description:
context["content"] = self.description
if self.license:
context["license"] = self.license
if show_comments:
comments = [comment.serialize(request) for comment in self.get_comments()]
total = len(comments)
context["replies"] = {
"totalItems": total,
"items": comments,
"url": request.urlgen(
"mediagoblin.federation.object.comments",
objectType=self.objectType,
id=self.id,
qualified=True
),
}
return context
def unserialize(self, data):
""" Takes API objects and unserializes on existing MediaEntry """
if "displayName" in data:
self.title = data["displayName"]
if "content" in data:
self.description = data["content"]
if "license" in data:
self.license = data["license"]
return True
class FileKeynames(Base):
"""
@ -528,6 +646,47 @@ class MediaComment(Base, MediaCommentMixin):
lazy="dynamic",
cascade="all, delete-orphan"))
def serialize(self, request):
""" Unserialize to python dictionary for API """
media = MediaEntry.query.filter_by(id=self.media_entry).first()
author = self.get_author
context = {
"id": self.id,
"objectType": "comment",
"content": self.content,
"inReplyTo": media.serialize(request, show_comments=False),
"author": author.serialize(request)
}
return context
def unserialize(self, data):
""" Takes API objects and unserializes on existing comment """
# Do initial checks to verify the object is correct
required_attributes = ["content", "inReplyTo"]
for attr in required_attributes:
if attr not in data:
return False
# Validate inReplyTo has ID
if "id" not in data["inReplyTo"]:
return False
# Validate that the ID is correct
try:
media_id = int(data["inReplyTo"]["id"])
except ValueError:
return False
media = MediaEntry.query.filter_by(id=media_id).first()
if media is None:
return False
self.media_entry = media.id
self.content = data["content"]
return True
class Collection(Base, CollectionMixin):
"""An 'album' or 'set' of media by a user.
@ -563,6 +722,14 @@ class Collection(Base, CollectionMixin):
return CollectionItem.query.filter_by(
collection=self.id).order_by(order_col)
def __repr__(self):
safe_title = self.title.encode('ascii', 'replace')
return '<{classname} #{id}: {title} by {creator}>'.format(
id=self.id,
classname=self.__class__.__name__,
creator=self.creator,
title=safe_title)
class CollectionItem(Base, CollectionItemMixin):
__tablename__ = "core__collection_items"
@ -592,6 +759,13 @@ class CollectionItem(Base, CollectionItemMixin):
"""A dict like view on this object"""
return DictReadAttrProxy(self)
def __repr__(self):
return '<{classname} #{id}: Entry {entry} in {collection}>'.format(
id=self.id,
classname=self.__class__.__name__,
collection=self.collection,
entry=self.media_entry)
class ProcessingMetaData(Base):
__tablename__ = 'core__processing_metadata'
@ -667,6 +841,14 @@ class Notification(Base):
subject=getattr(self, 'subject', None),
seen='unseen' if not self.seen else 'seen')
def __unicode__(self):
return u'<{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'
@ -871,13 +1053,13 @@ class PrivilegeUserAssociation(Base):
__tablename__ = 'core__privileges_users'
privilege_id = Column(
'core__privilege_id',
user = Column(
"user",
Integer,
ForeignKey(User.id),
primary_key=True)
user_id = Column(
'core__user_id',
privilege = Column(
"privilege",
Integer,
ForeignKey(Privilege.id),
primary_key=True)

View File

@ -76,11 +76,16 @@ def check_db_up_to_date():
dbdatas = gather_database_data(mgg.global_config.get('plugins', {}).keys())
for dbdata in dbdatas:
migration_manager = dbdata.make_migration_manager(Session())
if migration_manager.database_current_migration is None or \
migration_manager.migrations_to_run():
sys.exit("Your database is not up to date. Please run "
"'gmg dbupdate' before starting MediaGoblin.")
session = Session()
try:
migration_manager = dbdata.make_migration_manager(session)
if migration_manager.database_current_migration is None or \
migration_manager.migrations_to_run():
sys.exit("Your database is not up to date. Please run "
"'gmg dbupdate' before starting MediaGoblin.")
finally:
Session.rollback()
Session.remove()
if __name__ == '__main__':

View File

@ -23,7 +23,7 @@ from six.moves.urllib.parse import urljoin
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User, MediaComment
from mediagoblin.db.models import MediaEntry, User, MediaComment, AccessToken
from mediagoblin.tools.response import (
redirect, render_404,
render_user_banned, json_response)
@ -75,7 +75,7 @@ def require_active_login(controller):
return new_controller_func
def user_has_privilege(privilege_name):
def user_has_privilege(privilege_name, allow_admin=True):
"""
Requires that a user have a particular privilege in order to access a page.
In order to require that a user have multiple privileges, use this
@ -86,14 +86,17 @@ def user_has_privilege(privilege_name):
the privilege object. This object is
the name of the privilege, as assigned
in the Privilege.privilege_name column
:param allow_admin If this is true then if the user is an admin
it will allow the user even if the user doesn't
have the privilage given in privilage_name.
"""
def user_has_privilege_decorator(controller):
@wraps(controller)
@require_active_login
def wrapper(request, *args, **kwargs):
user_id = request.user.id
if not request.user.has_privilege(privilege_name):
if not request.user.has_privilege(privilege_name, allow_admin):
raise Forbidden()
return controller(request, *args, **kwargs)
@ -370,7 +373,8 @@ def require_admin_or_moderator_login(controller):
@wraps(controller)
def new_controller_func(request, *args, **kwargs):
if request.user and \
not request.user.has_privilege(u'admin',u'moderator'):
not (request.user.has_privilege(u'admin')
or request.user.has_privilege(u'moderator')):
raise Forbidden()
elif not request.user:
@ -402,10 +406,10 @@ def oauth_required(controller):
request_validator = GMGRequestValidator()
resource_endpoint = ResourceEndpoint(request_validator)
valid, request = resource_endpoint.validate_protected_resource_request(
valid, r = resource_endpoint.validate_protected_resource_request(
uri=request.url,
http_method=request.method,
body=request.get_data(),
body=request.data,
headers=dict(request.headers),
)
@ -413,6 +417,13 @@ def oauth_required(controller):
error = "Invalid oauth prarameter."
return json_response({"error": error}, status=400)
# Fill user if not already
token = authorization[u"oauth_token"]
access_token = AccessToken.query.filter_by(token=token).first()
if access_token is not None and request.user is None:
user_id = access_token.user
request.user = User.query.filter_by(id=user_id).first()
return controller(request, *args, **kwargs)
return wrapper

View File

@ -15,10 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import wtforms
from jsonschema import Draft4Validator
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.tools.metadata import DEFAULT_SCHEMA, DEFAULT_CHECKER
from mediagoblin.auth.tools import normalize_user_or_email_field
@ -38,7 +40,7 @@ class EditForm(wtforms.Form):
"Separate tags by commas."))
slug = wtforms.TextField(
_('Slug'),
[wtforms.validators.Required(message=_("The slug can't be empty"))],
[wtforms.validators.InputRequired(message=_("The slug can't be empty"))],
description=_(
"The title part of this media's address. "
"You usually don't need to change this."))
@ -85,7 +87,7 @@ class EditAttachmentsForm(wtforms.Form):
class EditCollectionForm(wtforms.Form):
title = wtforms.TextField(
_('Title'),
[wtforms.validators.Length(min=0, max=500), wtforms.validators.Required(message=_("The title can't be empty"))])
[wtforms.validators.Length(min=0, max=500), wtforms.validators.InputRequired(message=_("The title can't be empty"))])
description = wtforms.TextAreaField(
_('Description of this collection'),
description=_("""You can use
@ -93,7 +95,7 @@ class EditCollectionForm(wtforms.Form):
Markdown</a> for formatting."""))
slug = wtforms.TextField(
_('Slug'),
[wtforms.validators.Required(message=_("The slug can't be empty"))],
[wtforms.validators.InputRequired(message=_("The slug can't be empty"))],
description=_(
"The title part of this collection's address. "
"You usually don't need to change this."))
@ -102,12 +104,12 @@ class EditCollectionForm(wtforms.Form):
class ChangePassForm(wtforms.Form):
old_password = wtforms.PasswordField(
_('Old password'),
[wtforms.validators.Required()],
[wtforms.validators.InputRequired()],
description=_(
"Enter your old password to prove you own this account."))
new_password = wtforms.PasswordField(
_('New password'),
[wtforms.validators.Required(),
[wtforms.validators.InputRequired(),
wtforms.validators.Length(min=6, max=30)],
id="password")
@ -115,10 +117,45 @@ class ChangePassForm(wtforms.Form):
class ChangeEmailForm(wtforms.Form):
new_email = wtforms.TextField(
_('New email address'),
[wtforms.validators.Required(),
[wtforms.validators.InputRequired(),
normalize_user_or_email_field(allow_user=False)])
password = wtforms.PasswordField(
_('Password'),
[wtforms.validators.Required()],
[wtforms.validators.InputRequired()],
description=_(
"Enter your password to prove you own this account."))
class MetaDataValidator(object):
"""
Custom validator which runs form data in a MetaDataForm through a jsonschema
validator and passes errors recieved in jsonschema to wtforms.
:param schema The json schema to validate the data against. By
default this uses the DEFAULT_SCHEMA from
mediagoblin.tools.metadata.
:param format_checker The FormatChecker object that limits which types
jsonschema can recognize. By default this uses
DEFAULT_CHECKER from mediagoblin.tools.metadata.
"""
def __init__(self, schema=DEFAULT_SCHEMA, format_checker=DEFAULT_CHECKER):
self.schema = schema
self.format_checker = format_checker
def __call__(self, form, field):
metadata_dict = {field.data:form.value.data}
validator = Draft4Validator(self.schema,
format_checker=self.format_checker)
errors = [e.message
for e in validator.iter_errors(metadata_dict)]
if len(errors) >= 1:
raise wtforms.validators.ValidationError(
errors.pop())
class MetaDataForm(wtforms.Form):
identifier = wtforms.TextField(_(u'Identifier'),[MetaDataValidator()])
value = wtforms.TextField(_(u'Value'))
class EditMetaDataForm(wtforms.Form):
media_metadata = wtforms.FieldList(
wtforms.FormField(MetaDataForm, ""),
)

View File

@ -19,8 +19,10 @@ import six
from datetime import datetime
from itsdangerous import BadSignature
from pyld import jsonld
from werkzeug.exceptions import Forbidden
from werkzeug.utils import secure_filename
from jsonschema import ValidationError, Draft4Validator
from mediagoblin import messages
from mediagoblin import mg_globals
@ -31,8 +33,11 @@ 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)
get_user_collection, user_has_privilege,
user_not_banned)
from mediagoblin.tools.crypto import get_timed_signer_url
from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER,
DEFAULT_SCHEMA)
from mediagoblin.tools.mail import email_debug_message
from mediagoblin.tools.response import (render_to_response,
redirect, redirect_obj, render_404)
@ -434,3 +439,30 @@ def change_email(request):
'mediagoblin/edit/change_email.html',
{'form': form,
'user': user})
@user_has_privilege(u'admin')
@require_active_login
@get_media_entry_by_id
def edit_metadata(request, media):
form = forms.EditMetaDataForm(request.form)
if request.method == "POST" and form.validate():
metadata_dict = dict([(row['identifier'],row['value'])
for row in form.media_metadata.data])
json_ld_metadata = None
json_ld_metadata = compact_and_validate(metadata_dict)
media.media_metadata = json_ld_metadata
media.save()
return redirect_obj(request, media)
if len(form.media_metadata) == 0:
for identifier, value in media.media_metadata.iteritems():
if identifier == "@context": continue
form.media_metadata.append_entry({
'identifier':identifier,
'value':value})
return render_to_response(
request,
'mediagoblin/edit/metadata.html',
{'form':form,
'media':media})

View File

@ -13,13 +13,3 @@
#
# 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/>.
'''
mediagoblin.webfinger_ provides an LRDD discovery service and
a web host meta information file
Links:
- `LRDD Discovery Draft
<http://tools.ietf.org/html/draft-hammer-discovery-06>`_.
- `RFC 6415 - Web Host Metadata
<http://tools.ietf.org/html/rfc6415>`_.
'''

View File

@ -0,0 +1,49 @@
# 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 functools import wraps
from mediagoblin.db.models import User
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.response import json_response
def user_has_privilege(privilege_name):
"""
Requires that a user have a particular privilege in order to access a page.
In order to require that a user have multiple privileges, use this
decorator twice on the same view. This decorator also makes sure that the
user is not banned, or else it redirects them to the "You are Banned" page.
:param privilege_name A unicode object that is that represents
the privilege object. This object is
the name of the privilege, as assigned
in the Privilege.privilege_name column
"""
def user_has_privilege_decorator(controller):
@wraps(controller)
@require_active_login
def wrapper(request, *args, **kwargs):
if not request.user.has_privilege(privilege_name):
error = "User '{0}' needs '{1}' privilege".format(
request.user.username,
privilege_name
)
return json_response({"error": error}, status=403)
return controller(request, *args, **kwargs)
return wrapper
return user_has_privilege_decorator

View File

@ -0,0 +1,79 @@
# 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 user profile
add_route(
"mediagoblin.federation.user",
"/api/user/<string:username>/",
"mediagoblin.federation.views:user_endpoint"
)
add_route(
"mediagoblin.federation.user.profile",
"/api/user/<string:username>/profile",
"mediagoblin.federation.views:profile_endpoint"
)
# Inbox and Outbox (feed)
add_route(
"mediagoblin.federation.feed",
"/api/user/<string:username>/feed",
"mediagoblin.federation.views:feed_endpoint"
)
add_route(
"mediagoblin.federation.user.uploads",
"/api/user/<string:username>/uploads",
"mediagoblin.federation.views:uploads_endpoint"
)
add_route(
"mediagoblin.federation.inbox",
"/api/user/<string:username>/inbox",
"mediagoblin.federation.views:feed_endpoint"
)
# object endpoints
add_route(
"mediagoblin.federation.object",
"/api/<string:objectType>/<string:id>",
"mediagoblin.federation.views:object_endpoint"
)
add_route(
"mediagoblin.federation.object.comments",
"/api/<string:objectType>/<string:id>/comments",
"mediagoblin.federation.views:object_comments"
)
add_route(
"mediagoblin.webfinger.well-known.host-meta",
"/.well-known/host-meta",
"mediagoblin.federation.views:host_meta"
)
add_route(
"mediagoblin.webfinger.well-known.host-meta.json",
"/.well-known/host-meta.json",
"mediagoblin.federation.views:host_meta"
)
add_route(
"mediagoblin.webfinger.whoami",
"/api/whoami",
"mediagoblin.federation.views:whoami"
)

View File

@ -0,0 +1,469 @@
# 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 json
import io
import mimetypes
from werkzeug.datastructures import FileStorage
from mediagoblin.decorators import oauth_required
from mediagoblin.federation.decorators import user_has_privilege
from mediagoblin.db.models import User, MediaEntry, MediaComment
from mediagoblin.tools.response import redirect, json_response, json_error
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \
api_add_to_feed
# MediaTypes
from mediagoblin.media_types.image import MEDIA_TYPE as IMAGE_MEDIA_TYPE
# Getters
def get_profile(request):
"""
Gets the user's profile for the endpoint requested.
For example an endpoint which is /api/{username}/feed
as /api/cwebber/feed would get cwebber's profile. This
will return a tuple (username, user_profile). If no user
can be found then this function returns a (None, None).
"""
username = request.matchdict["username"]
user = User.query.filter_by(username=username).first()
if user is None:
return None, None
return user, user.serialize(request)
# Endpoints
@oauth_required
def profile_endpoint(request):
""" This is /api/user/<username>/profile - This will give profile info """
user, user_profile = get_profile(request)
if user is None:
username = request.matchdict["username"]
return json_error(
"No such 'user' with username '{0}'".format(username),
status=404
)
# user profiles are public so return information
return json_response(user_profile)
@oauth_required
def user_endpoint(request):
""" This is /api/user/<username> - This will get the user """
user, user_profile = get_profile(request)
if user is None:
username = request.matchdict["username"]
return json_error(
"No such 'user' with username '{0}'".format(username),
status=404
)
return json_response({
"nickname": user.username,
"updated": user.created.isoformat(),
"published": user.created.isoformat(),
"profile": user_profile,
})
@oauth_required
@csrf_exempt
@user_has_privilege(u'uploader')
def uploads_endpoint(request):
""" Endpoint for file uploads """
username = request.matchdict["username"]
requested_user = User.query.filter_by(username=username).first()
if requested_user is None:
return json_error("No such 'user' with id '{0}'".format(username), 404)
if request.method == "POST":
# Ensure that the user is only able to upload to their own
# upload endpoint.
if requested_user.id != request.user.id:
return json_error(
"Not able to post to another users feed.",
status=403
)
# Wrap the data in the werkzeug file wrapper
if "Content-Type" not in request.headers:
return json_error(
"Must supply 'Content-Type' header to upload media."
)
mimetype = request.headers["Content-Type"]
filename = mimetypes.guess_all_extensions(mimetype)
filename = 'unknown' + filename[0] if filename else filename
file_data = FileStorage(
stream=io.BytesIO(request.data),
filename=filename,
content_type=mimetype
)
# Find media manager
entry = new_upload_entry(request.user)
entry.media_type = IMAGE_MEDIA_TYPE
return api_upload_request(request, file_data, entry)
return json_error("Not yet implemented", 501)
@oauth_required
@csrf_exempt
def feed_endpoint(request):
""" Handles the user's outbox - /api/user/<username>/feed """
username = request.matchdict["username"]
requested_user = User.query.filter_by(username=username).first()
# check if the user exists
if requested_user is None:
return json_error("No such 'user' with id '{0}'".format(username), 404)
if request.data:
data = json.loads(request.data)
else:
data = {"verb": None, "object": {}}
if request.method in ["POST", "PUT"]:
# Validate that the activity is valid
if "verb" not in data or "object" not in data:
return json_error("Invalid activity provided.")
# Check that the verb is valid
if data["verb"] not in ["post", "update"]:
return json_error("Verb not yet implemented", 501)
# We need to check that the user they're posting to is
# the person that they are.
if requested_user.id != request.user.id:
return json_error(
"Not able to post to another users feed.",
status=403
)
# Handle new posts
if data["verb"] == "post":
obj = data.get("object", None)
if obj is None:
return json_error("Could not find 'object' element.")
if obj.get("objectType", None) == "comment":
# post a comment
if not request.user.has_privilege(u'commenter'):
return json_error(
"Privilege 'commenter' required to comment.",
status=403
)
comment = MediaComment(author=request.user.id)
comment.unserialize(data["object"])
comment.save()
data = {
"verb": "post",
"object": comment.serialize(request)
}
return json_response(data)
elif obj.get("objectType", None) == "image":
# Posting an image to the feed
media_id = int(data["object"]["id"])
media = MediaEntry.query.filter_by(id=media_id).first()
if media is None:
return json_response(
"No such 'image' with id '{0}'".format(media_id),
status=404
)
if media.uploader != request.user.id:
return json_error(
"Privilege 'commenter' required to comment.",
status=403
)
if not media.unserialize(data["object"]):
return json_error(
"Invalid 'image' with id '{0}'".format(media_id)
)
media.save()
api_add_to_feed(request, media)
return json_response({
"verb": "post",
"object": media.serialize(request)
})
elif obj.get("objectType", None) is None:
# They need to tell us what type of object they're giving us.
return json_error("No objectType specified.")
else:
# Oh no! We don't know about this type of object (yet)
object_type = obj.get("objectType", None)
return json_error(
"Unknown object type '{0}'.".format(object_type)
)
# Updating existing objects
if data["verb"] == "update":
# Check we've got a valid object
obj = data.get("object", None)
if obj is None:
return json_error("Could not find 'object' element.")
if "objectType" not in obj:
return json_error("No objectType specified.")
if "id" not in obj:
return json_error("Object ID has not been specified.")
obj_id = obj["id"]
# Now try and find object
if obj["objectType"] == "comment":
if not request.user.has_privilege(u'commenter'):
return json_error(
"Privilege 'commenter' required to comment.",
status=403
)
comment = MediaComment.query.filter_by(id=obj_id).first()
if comment is None:
return json_error(
"No such 'comment' with id '{0}'.".format(obj_id)
)
# Check that the person trying to update the comment is
# the author of the comment.
if comment.author != request.user.id:
return json_error(
"Only author of comment is able to update comment.",
status=403
)
if not comment.unserialize(data["object"]):
return json_error(
"Invalid 'comment' with id '{0}'".format(obj_id)
)
comment.save()
activity = {
"verb": "update",
"object": comment.serialize(request),
}
return json_response(activity)
elif obj["objectType"] == "image":
image = MediaEntry.query.filter_by(id=obj_id).first()
if image is None:
return json_error(
"No such 'image' with the id '{0}'.".format(obj_id)
)
# Check that the person trying to update the comment is
# the author of the comment.
if image.uploader != request.user.id:
return json_error(
"Only uploader of image is able to update image.",
status=403
)
if not image.unserialize(obj):
return json_error(
"Invalid 'image' with id '{0}'".format(obj_id)
)
image.save()
activity = {
"verb": "update",
"object": image.serialize(request),
}
return json_response(activity)
elif request.method != "GET":
return json_error(
"Unsupported HTTP method {0}".format(request.method),
status=501
)
feed_url = request.urlgen(
"mediagoblin.federation.feed",
username=request.user.username,
qualified=True
)
feed = {
"displayName": "Activities by {user}@{host}".format(
user=request.user.username,
host=request.host
),
"objectTypes": ["activity"],
"url": feed_url,
"links": {
"first": {
"href": feed_url,
},
"self": {
"href": request.url,
},
"prev": {
"href": feed_url,
},
"next": {
"href": feed_url,
}
},
"author": request.user.serialize(request),
"items": [],
}
# Look up all the media to put in the feed (this will be changed
# when we get real feeds/inboxes/outboxes/activites)
for media in MediaEntry.query.all():
item = {
"verb": "post",
"object": media.serialize(request),
"actor": media.get_uploader.serialize(request),
"content": "{0} posted a picture".format(request.user.username),
"id": media.id,
}
item["updated"] = item["object"]["updated"]
item["published"] = item["object"]["published"]
item["url"] = item["object"]["url"]
feed["items"].append(item)
feed["totalItems"] = len(feed["items"])
return json_response(feed)
@oauth_required
def object_endpoint(request):
""" Lookup for a object type """
object_type = request.matchdict["objectType"]
try:
object_id = int(request.matchdict["id"])
except ValueError:
error = "Invalid object ID '{0}' for '{1}'".format(
request.matchdict["id"],
object_type
)
return json_error(error)
if object_type not in ["image"]:
# not sure why this is 404, maybe ask evan. Maybe 400?
return json_error(
"Unknown type: {0}".format(object_type),
status=404
)
media = MediaEntry.query.filter_by(id=object_id).first()
if media is None:
return json_error(
"Can't find '{0}' with ID '{1}'".format(object_type, object_id),
status=404
)
return json_response(media.serialize(request))
@oauth_required
def object_comments(request):
""" Looks up for the comments on a object """
media = MediaEntry.query.filter_by(id=request.matchdict["id"]).first()
if media is None:
return json_error("Can't find '{0}' with ID '{1}'".format(
request.matchdict["objectType"],
request.matchdict["id"]
), 404)
comments = response.serialize(request)
comments = comments.get("replies", {
"totalItems": 0,
"items": [],
"url": request.urlgen(
"mediagoblin.federation.object.comments",
objectType=media.objectType,
id=media.id,
qualified=True
)
})
comments["displayName"] = "Replies to {0}".format(comments["url"])
comments["links"] = {
"first": comments["url"],
"self": comments["url"],
}
return json_response(comments)
##
# Well known
##
def host_meta(request):
""" /.well-known/host-meta - provide URLs to resources """
links = []
links.append({
"ref": "registration_endpoint",
"href": request.urlgen(
"mediagoblin.oauth.client_register",
qualified=True
),
})
links.append({
"ref": "http://apinamespace.org/oauth/request_token",
"href": request.urlgen(
"mediagoblin.oauth.request_token",
qualified=True
),
})
links.append({
"ref": "http://apinamespace.org/oauth/authorize",
"href": request.urlgen(
"mediagoblin.oauth.authorize",
qualified=True
),
})
links.append({
"ref": "http://apinamespace.org/oauth/access_token",
"href": request.urlgen(
"mediagoblin.oauth.access_token",
qualified=True
),
})
return json_response({"links": links})
def whoami(request):
""" /api/whoami - HTTP redirect to API profile """
if request.user is None:
return json_error("Not logged in.", status=401)
profile = request.urlgen(
"mediagoblin.federation.user.profile",
username=request.user.username,
qualified=True
)
return redirect(request, location=profile)

View File

@ -39,6 +39,10 @@ SUBCOMMAND_MAP = {
'setup': 'mediagoblin.gmg_commands.users:changepw_parser_setup',
'func': 'mediagoblin.gmg_commands.users:changepw',
'help': 'Changes a user\'s password'},
'deleteuser': {
'setup': 'mediagoblin.gmg_commands.users:deleteuser_parser_setup',
'func': 'mediagoblin.gmg_commands.users:deleteuser',
'help': 'Deletes a user'},
'dbupdate': {
'setup': 'mediagoblin.gmg_commands.dbupdate:dbupdate_parse_setup',
'func': 'mediagoblin.gmg_commands.dbupdate:dbupdate',
@ -63,6 +67,10 @@ SUBCOMMAND_MAP = {
'setup': 'mediagoblin.gmg_commands.serve:parser_setup',
'func': 'mediagoblin.gmg_commands.serve:serve',
'help': 'PasteScript replacement'},
'batchaddmedia': {
'setup': 'mediagoblin.gmg_commands.batchaddmedia:parser_setup',
'func': 'mediagoblin.gmg_commands.batchaddmedia:batchaddmedia',
'help': 'Add many media entries at once'},
# 'theme': {
# 'setup': 'mediagoblin.gmg_commands.theme:theme_parser_setup',
# 'func': 'mediagoblin.gmg_commands.theme:theme',

View File

@ -0,0 +1,206 @@
# 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 requests, codecs
import csv
from urlparse import urlparse
from mediagoblin.gmg_commands import util as commands_util
from mediagoblin.submit.lib import (
submit_media, get_upload_file_limits,
FileUploadLimit, UserUploadLimit, UserPastUploadLimit)
from mediagoblin.tools.metadata import compact_and_validate
from mediagoblin.tools.translate import pass_to_ugettext as _
from jsonschema.exceptions import ValidationError
def parser_setup(subparser):
subparser.description = """\
This command allows the administrator to upload many media files at once."""
subparser.epilog = _(u"""For more information about how to properly run this
script (and how to format the metadata csv file), read the MediaGoblin
documentation page on command line uploading
<http://docs.mediagoblin.org/siteadmin/commandline-upload.html>""")
subparser.add_argument(
'username',
help=_(u"Name of user these media entries belong to"))
subparser.add_argument(
'metadata_path',
help=_(
u"""Path to the csv file containing metadata information."""))
subparser.add_argument(
'--celery',
action='store_true',
help=_(u"Don't process eagerly, pass off to celery"))
def batchaddmedia(args):
# Run eagerly unless explicetly set not to
if not args.celery:
os.environ['CELERY_ALWAYS_EAGER'] = 'true'
app = commands_util.setup_app(args)
files_uploaded, files_attempted = 0, 0
# get the user
user = app.db.User.query.filter_by(username=args.username.lower()).first()
if user is None:
print _(u"Sorry, no user by username '{username}' exists".format(
username=args.username))
return
upload_limit, max_file_size = get_upload_file_limits(user)
temp_files = []
if os.path.isfile(args.metadata_path):
metadata_path = args.metadata_path
else:
error = _(u'File at {path} not found, use -h flag for help'.format(
path=args.metadata_path))
print error
return
abs_metadata_filename = os.path.abspath(metadata_path)
abs_metadata_dir = os.path.dirname(abs_metadata_filename)
upload_limit, max_file_size = get_upload_file_limits(user)
def maybe_unicodeify(some_string):
# this is kinda terrible
if some_string is None:
return None
else:
return unicode(some_string)
with codecs.open(
abs_metadata_filename, 'r', encoding='utf-8') as all_metadata:
contents = all_metadata.read()
media_metadata = parse_csv_file(contents)
for media_id, file_metadata in media_metadata.iteritems():
files_attempted += 1
# In case the metadata was not uploaded initialize an empty dictionary.
json_ld_metadata = compact_and_validate({})
# Get all metadata entries starting with 'media' as variables and then
# delete them because those are for internal use only.
original_location = file_metadata['location']
### Pull the important media information for mediagoblin from the
### metadata, if it is provided.
title = file_metadata.get('title') or file_metadata.get('dc:title')
description = (file_metadata.get('description') or
file_metadata.get('dc:description'))
license = file_metadata.get('license')
try:
json_ld_metadata = compact_and_validate(file_metadata)
except ValidationError, exc:
error = _(u"""Error with media '{media_id}' value '{error_path}': {error_msg}
Metadata was not uploaded.""".format(
media_id=media_id,
error_path=exc.path[0],
error_msg=exc.message))
print error
continue
url = urlparse(original_location)
filename = url.path.split()[-1]
if url.scheme == 'http':
res = requests.get(url.geturl(), stream=True)
media_file = res.raw
elif url.scheme == '':
path = url.path
if os.path.isabs(path):
file_abs_path = os.path.abspath(path)
else:
file_path = os.path.join(abs_metadata_dir, path)
file_abs_path = os.path.abspath(file_path)
try:
media_file = file(file_abs_path, 'r')
except IOError:
print _(u"""\
FAIL: Local file {filename} could not be accessed.
{filename} will not be uploaded.""".format(filename=filename))
continue
try:
submit_media(
mg_app=app,
user=user,
submitted_file=media_file,
filename=filename,
title=maybe_unicodeify(title),
description=maybe_unicodeify(description),
license=maybe_unicodeify(license),
metadata=json_ld_metadata,
tags_string=u"",
upload_limit=upload_limit, max_file_size=max_file_size)
print _(u"""Successfully submitted {filename}!
Be sure to look at the Media Processing Panel on your website to be sure it
uploaded successfully.""".format(filename=filename))
files_uploaded += 1
except FileUploadLimit:
print _(
u"FAIL: This file is larger than the upload limits for this site.")
except UserUploadLimit:
print _(
"FAIL: This file will put this user past their upload limits.")
except UserPastUploadLimit:
print _("FAIL: This user is already past their upload limits.")
print _(
"{files_uploaded} out of {files_attempted} files successfully submitted".format(
files_uploaded=files_uploaded,
files_attempted=files_attempted))
def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
# csv.py doesn't do Unicode; encode temporarily as UTF-8:
csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
dialect=dialect, **kwargs)
for row in csv_reader:
# decode UTF-8 back to Unicode, cell by cell:
yield [unicode(cell, 'utf-8') for cell in row]
def utf_8_encoder(unicode_csv_data):
for line in unicode_csv_data:
yield line.encode('utf-8')
def parse_csv_file(file_contents):
"""
The helper function which converts the csv file into a dictionary where each
item's key is the provided value 'id' and each item's value is another
dictionary.
"""
list_of_contents = file_contents.split('\n')
key, lines = (list_of_contents[0].split(','),
list_of_contents[1:])
objects_dict = {}
# Build a dictionary
for index, line in enumerate(lines):
if line.isspace() or line == u'': continue
values = unicode_csv_reader([line]).next()
line_dict = dict([(key[i], val)
for i, val in enumerate(values)])
media_id = line_dict.get('id') or index
objects_dict[media_id] = (line_dict)
return objects_dict

View File

@ -15,19 +15,23 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import sys
from mediagoblin.gmg_commands import util as commands_util
def parser_setup(subparser):
subparser.add_argument('media_ids',
help='Comma separated list of media IDs to will be deleted.')
help='Comma separated list of media IDs will be deleted.')
def deletemedia(args):
app = commands_util.setup_app(args)
media_ids = set(map(int, args.media_ids.split(',')))
media_ids = set([int(mid) for mid in args.media_ids.split(',') if mid.isdigit()])
if not media_ids:
print 'Can\'t find any valid media ID(s).'
sys.exit(1)
found_medias = set()
filter_ids = app.db.MediaEntry.id.in_(media_ids)
medias = app.db.MediaEntry.query.filter(filter_ids).all()
@ -38,3 +42,4 @@ def deletemedia(args):
for media in media_ids - found_medias:
print('Can\'t find a media with ID %d.' % media)
print('Done.')
sys.exit(0)

View File

@ -38,7 +38,7 @@ def adduser(args):
#TODO: Lets trust admins this do not validate Emails :)
commands_util.setup_app(args)
args.username = commands_util.prompt_if_not_set(args.username, "Username:")
args.username = unicode(commands_util.prompt_if_not_set(args.username, "Username:"))
args.password = commands_util.prompt_if_not_set(args.password, "Password:",True)
args.email = commands_util.prompt_if_not_set(args.email, "Email:")
@ -119,3 +119,23 @@ def changepw(args):
print(u'Password successfully changed')
else:
print(u'The user doesn\'t exist')
def deleteuser_parser_setup(subparser):
subparser.add_argument(
'username',
help="Username to delete")
def deleteuser(args):
commands_util.setup_app(args)
db = mg_globals.database
user = db.User.query.filter_by(
username=unicode(args.username.lower())).one()
if user:
user.delete()
print('The user %s has been deleted' % args.username)
else:
print('The user %s doesn\'t exist' % args.username)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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