Merge branch 'master' of git://gitorious.org/mediagoblin/mediagoblin
This commit is contained in:
commit
e02b7b6b3b
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[submodule "pdf.js"]
|
||||
path = pdf.js
|
||||
url = git://github.com/mozilla/pdf.js.git
|
||||
[submodule "extlib/pdf.js"]
|
||||
path = extlib/pdf.js
|
||||
url = git://github.com/mozilla/pdf.js.git
|
29
AUTHORS
29
AUTHORS
@ -8,31 +8,60 @@ variety of different ways and this software wouldn't exist without them.
|
||||
Thank you!
|
||||
|
||||
* Aaron Williamson
|
||||
* Aeva Ntsc
|
||||
* Alejandro Villanueva
|
||||
* Aleksandar Micovic
|
||||
* Aleksej Serdjukov
|
||||
* Alex Camelio
|
||||
* András Veres-Szentkirályi
|
||||
* Bassam Kurdali
|
||||
* Bernhard Keller
|
||||
* Brett Smith
|
||||
* Caleb Forbes Davis V
|
||||
* Corey Farwell
|
||||
* Chris Moylan
|
||||
* Christopher Allan Webber
|
||||
* Daniel Neel
|
||||
* Deb Nicholson
|
||||
* Derek Moore
|
||||
* Duncan Paterson
|
||||
* Elrond of Samba TNG
|
||||
* Emily O'Leary
|
||||
* Greg Grossmeier
|
||||
* Jakob Kramer
|
||||
* Jef van Schendel
|
||||
* Jessica Tallon
|
||||
* Jim Campbell
|
||||
* Joar Wandborg
|
||||
* Jorge Araya Navarro
|
||||
* Karen Rustad
|
||||
* Kuno Woudt
|
||||
* Larisa Hoffenbecker
|
||||
* Luke Slater
|
||||
* Manuel Urbano Santos
|
||||
* Mark Holmquist
|
||||
* Matt Lee
|
||||
* Michele Azzolari
|
||||
* Nathan Yergler
|
||||
* Odin Hørthe Omdal
|
||||
* Osama Khalid
|
||||
* Pablo J. Urbano Santos
|
||||
* Rasmus Larsson
|
||||
* Runar Petursson
|
||||
* Sacha De'Angeli
|
||||
* Sam Kleinman
|
||||
* Sebastian Spaeth
|
||||
* Shawn Khan
|
||||
* Stefano Zacchiroli
|
||||
* Tiberiu C. Turbureanu
|
||||
* Tran Thanh Bao
|
||||
* Shawn Khan
|
||||
* Will Kahn-Greene
|
||||
|
||||
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!
|
15
FOO300
15
FOO300
@ -1,15 +0,0 @@
|
||||
|
||||
This certifies that GNU MediaGoblin has been given the designation of:
|
||||
|
||||
FOO 300
|
||||
|
||||
In the Foo Communications ("FooCorp") catalogue of permanent record.
|
||||
|
||||
Signed:
|
||||
|
||||
|
||||
|
||||
|
||||
Matt Lee
|
||||
|
||||
Foo Communications, LLC
|
12
MANIFEST.in
12
MANIFEST.in
@ -1,5 +1,11 @@
|
||||
recursive-include mediagoblin/templates *.html *.txt
|
||||
recursive-include mediagoblin/static *.js *.css *.png *.svg *.ico
|
||||
recursive-include mediagoblin/tests *.ini
|
||||
recursive-include mediagoblin/i18n *.mo
|
||||
recursive-include mediagoblin *.js *.css *.png *.svg *.ico
|
||||
recursive-include mediagoblin *.ini
|
||||
recursive-include mediagoblin *.html *.txt
|
||||
recursive-include docs *.rst *.html
|
||||
include mediagoblin.ini mediagoblin/config_spec.ini paste.ini
|
||||
include mediagoblin/config_spec.ini
|
||||
graft extlib
|
||||
graft licenses
|
||||
include COPYING AUTHORS
|
||||
include lazyserver.sh lazystarter.sh lazycelery.sh
|
||||
|
@ -8,7 +8,7 @@ SPHINXAPIDOC = sphinx-apidoc
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
SOURCEDIR = source
|
||||
MEDIAGOBLIN_SOURCEDIR = ../
|
||||
MEDIAGOBLIN_SOURCEDIR = ../mediagoblin
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Extraction from Python source files
|
||||
[python: mediagoblin/**.py]
|
||||
# Extraction from Genshi HTML and text templates
|
||||
[jinja2: mediagoblin/templates/**.html]
|
||||
[jinja2: mediagoblin/**/templates/**.html]
|
||||
# Extract jinja templates (html)
|
||||
encoding = utf-8
|
||||
extensions = jinja2.ext.autoescape
|
||||
extensions = jinja2.ext.autoescape, mediagoblin.tools.template.TemplateHookExtension
|
||||
|
||||
[jinja2: mediagoblin/templates/**.txt]
|
||||
# Extract jinja templates (text)
|
||||
|
@ -161,6 +161,9 @@ then
|
||||
rm -rf docs/_build/
|
||||
fi
|
||||
|
||||
# Remove .pyc files that may have been generated by sphinx
|
||||
find mediagoblin -name '*.pyc' -exec rm {} \;
|
||||
|
||||
popd
|
||||
|
||||
tar -cvf $FNBASE.tar $FNBASE
|
||||
|
@ -26,7 +26,8 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = []
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
|
||||
intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None)}
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['source/_templates']
|
||||
|
@ -34,7 +34,81 @@ various recipes for getting things done.
|
||||
for where we hang out.
|
||||
|
||||
For more information on how to get started hacking on GNU MediaGoblin,
|
||||
see `the wiki <http://wiki.mediagoblin.org/>`_.
|
||||
see `the wiki <http://wiki.mediagoblin.org/>`_, and specifically, go
|
||||
through the
|
||||
`Hacking HOWTO <http://wiki.mediagoblin.org/HackingHowto>`_
|
||||
which explains generally how to get going with running an instance for
|
||||
development.
|
||||
|
||||
|
||||
What's where
|
||||
============
|
||||
|
||||
After you've run checked out mediagoblin and followed the virtualenv
|
||||
instantiation instructions, you're faced with the following directory
|
||||
tree::
|
||||
|
||||
mediagoblin/
|
||||
|- mediagoblin/ # source code
|
||||
| |- db/ # database setup
|
||||
| |- tools/ # various utilities
|
||||
| |- init/ # "initialization" tools (arguably should be in tools/)
|
||||
| |- tests/ # unit tests
|
||||
| |- templates/ # templates for this application
|
||||
| |- media_types/ # code for processing, displaying different media
|
||||
| |- storage/ # different storage backends
|
||||
| |- gmg_commands/ # command line tools (./bin/gmg)
|
||||
| |- themes/ # pre-bundled themes
|
||||
| |
|
||||
| | # ... some submodules here as well for different sections
|
||||
| | # of the application... here's just a few
|
||||
| |- auth/ # authentication (login/registration) code
|
||||
| |- user_dev/ # user pages (under /u/), including media pages
|
||||
| \- submit/ # submitting media for processing
|
||||
|
|
||||
|- docs/ # documentation
|
||||
|- devtools/ # some scripts for developer convenience
|
||||
|
|
||||
|- user_dev/ # local instance sessions, media, etc
|
||||
|
|
||||
| # the below directories are installed into your virtualenv checkout
|
||||
|
|
||||
|- bin/ # scripts
|
||||
|- develop-eggs/
|
||||
|- lib/ # python libraries installed into your virtualenv
|
||||
|- include/
|
||||
|- mediagoblin.egg-info/
|
||||
\- parts/
|
||||
|
||||
|
||||
As you can see, all the code for GNU MediaGoblin is in the
|
||||
``mediagoblin`` directory.
|
||||
|
||||
Here are some interesting files and what they do:
|
||||
|
||||
:routing.py: maps url paths to views
|
||||
:views.py: views handle http requests
|
||||
:forms.py: wtforms stuff for this submodule
|
||||
|
||||
You'll notice that there are several sub-directories: tests,
|
||||
templates, auth, submit, ...
|
||||
|
||||
``tests`` holds the unit test code.
|
||||
|
||||
``templates`` holds all the templates for the output.
|
||||
|
||||
``auth`` and ``submit`` are modules that enacpsulate authentication
|
||||
and media item submission. If you look in these directories, you'll
|
||||
see they have their own ``routing.py``, ``view.py``, and forms.py in
|
||||
addition to some other code.
|
||||
|
||||
You'll also notice that mediagoblin/db/ contains quite a few things,
|
||||
including the following:
|
||||
|
||||
:models.py: This is where the database is set up
|
||||
:mixin.py: Certain functions appended to models from here
|
||||
:migrations.py: When creating a new migration (a change to the
|
||||
database structure), we put it here
|
||||
|
||||
|
||||
Software Stack
|
||||
@ -45,7 +119,7 @@ Software Stack
|
||||
* `Python <http://python.org/>`_: the language we're using to write
|
||||
this
|
||||
|
||||
* `Nose <http://somethingaboutorange.com/mrl/projects/nose/>`_:
|
||||
* `Py.Test <http://pytest.org/>`_:
|
||||
for unit tests
|
||||
|
||||
* `virtualenv <http://www.virtualenv.org/>`_: for setting up an
|
||||
@ -65,13 +139,11 @@ Software Stack
|
||||
`Paste Script <http://pythonpaste.org/script/>`_: we'll use this for
|
||||
configuring and launching the application
|
||||
|
||||
* `WebOb <http://pythonpaste.org/webob/>`_: nice abstraction layer
|
||||
* `werkzeug <http://werkzeug.pocoo.org/>`_: nice abstraction layer
|
||||
from HTTP requests, responses and WSGI bits
|
||||
|
||||
* `Routes <http://routes.groovie.org/>`_: for URL routing
|
||||
|
||||
* `Beaker <http://beaker.groovie.org/>`_: for handling sessions and
|
||||
caching
|
||||
* `itsdangerous <http://pythonhosted.org/itsdangerous/>`_:
|
||||
for handling sessions
|
||||
|
||||
* `Jinja2 <http://jinja.pocoo.org/docs/>`_: the templating engine
|
||||
|
||||
@ -109,52 +181,3 @@ Software Stack
|
||||
* `JQuery <http://jquery.com/>`_: for groovy JavaScript things
|
||||
|
||||
|
||||
|
||||
What's where
|
||||
============
|
||||
|
||||
After you've run checked out mediagoblin and followed the virtualenv
|
||||
instantiation instructions, you're faced with the following directory
|
||||
tree::
|
||||
|
||||
mediagoblin/
|
||||
|- mediagoblin/ # source code
|
||||
| |- tests/
|
||||
| |- templates/
|
||||
| |- auth/
|
||||
| \- submit/
|
||||
|- docs/ # documentation
|
||||
|- devtools/ # some scripts for developer convenience
|
||||
|
|
||||
| # the below directories are installed into your virtualenv checkout
|
||||
|
|
||||
|- bin/ # scripts
|
||||
|- develop-eggs/
|
||||
|- lib/ # python libraries installed into your virtualenv
|
||||
|- include/
|
||||
|- mediagoblin.egg-info/
|
||||
|- parts/
|
||||
|- user_dev/ # sessions, etc
|
||||
|
||||
|
||||
As you can see, all the code for GNU MediaGoblin is in the
|
||||
``mediagoblin`` directory.
|
||||
|
||||
Here are some interesting files and what they do:
|
||||
|
||||
:routing.py: maps url paths to views
|
||||
:views.py: views handle http requests
|
||||
:models.py: holds the sqlalchemy schemas---these are the data structures
|
||||
we're working with
|
||||
|
||||
You'll notice that there are several sub-directories: tests,
|
||||
templates, auth, submit, ...
|
||||
|
||||
``tests`` holds the unit test code.
|
||||
|
||||
``templates`` holds all the templates for the output.
|
||||
|
||||
``auth`` and ``submit`` are modules that enacpsulate authentication
|
||||
and media item submission. If you look in these directories, you'll
|
||||
see they have their own ``routing.py``, ``view.py``, and
|
||||
``models.py`` in addition to some other code.
|
336
docs/source/devel/originaldesigndecisions.rst
Normal file
336
docs/source/devel/originaldesigndecisions.rst
Normal file
@ -0,0 +1,336 @@
|
||||
.. _original-design-decisions-chapter:
|
||||
|
||||
===========================
|
||||
Original Design Decisions
|
||||
===========================
|
||||
|
||||
.. contents:: Sections
|
||||
:local:
|
||||
|
||||
|
||||
This chapter talks a bit about design decisions.
|
||||
|
||||
Note: This is an outdated document. It's more or less the historical
|
||||
reasons for a lot of things. That doesn't mean these decisions have
|
||||
stayed the same or we haven't changed our minds on some things!
|
||||
|
||||
|
||||
Why GNU MediaGoblin?
|
||||
====================
|
||||
|
||||
Chris and Will on "Why GNU MediaGoblin":
|
||||
|
||||
Chris came up with the name MediaGoblin. The name is pretty fun.
|
||||
It merges the idea that this is a Media hosting project with
|
||||
Goblin which sort of sounds like gobbling. Here's a piece of
|
||||
software that gobbles up your media for all to see.
|
||||
|
||||
`According to Wikipedia <http://en.wikipedia.org/wiki/Goblin>`_, a
|
||||
goblin is:
|
||||
|
||||
a legendary evil or mischievous illiterate creature, described
|
||||
as grotesquely evil or evil-like phantom
|
||||
|
||||
So are we evil? No. Are we mischievous or illiterate? Not
|
||||
really. So what kind of goblin are we thinking about? We're
|
||||
thinking about these goblins:
|
||||
|
||||
.. figure:: ../_static/goblin.png
|
||||
:alt: Cute goblin with a beret.
|
||||
|
||||
*Figure 1: Cute goblin with a beret. llustrated by Chris
|
||||
Webber*
|
||||
|
||||
.. figure:: ../_static/snugglygoblin.png
|
||||
:scale: 50%
|
||||
:alt: Snuggly goblin with a beret.
|
||||
|
||||
*Figure 2: Snuggly goblin. Illustrated by Karen Rustad*
|
||||
|
||||
Those are pretty cute goblins. Those are the kinds of goblins
|
||||
we're thinking about.
|
||||
|
||||
Chris started doing work on the project after thinking about it
|
||||
for a year. Then, after talking with Matt and Rob, it became an
|
||||
official GNU project. Thus we now call it GNU MediaGoblin.
|
||||
|
||||
That's a lot of letters, though, so in the interest of brevity and
|
||||
facilitating easier casual conversation and balancing that with
|
||||
what's important to us, we have the following rules:
|
||||
|
||||
1. "GNU MediaGoblin" is the name we're going to use in all official
|
||||
capacities: web site, documentation, press releases, ...
|
||||
|
||||
2. In casual conversation, it's ok to use more casual names.
|
||||
|
||||
3. If you're writing about the project, we ask that you call it GNU
|
||||
MediaGoblin.
|
||||
|
||||
4. If you don't like the name, we kindly ask you to take a deep
|
||||
breath, think a happy thought about cute little goblins playing
|
||||
on a playground and taking cute pictures of themselves, and let
|
||||
it go. (Will added this one.)
|
||||
|
||||
|
||||
Why Python
|
||||
==========
|
||||
|
||||
Chris Webber on "Why Python":
|
||||
|
||||
Because I know Python, love Python, am capable of actually making
|
||||
this thing happen in Python (I've worked on a lot of large free
|
||||
software web applications before in Python, including `Miro
|
||||
Community`_, the `Miro Guide`_, a large portion of `Creative
|
||||
Commons`_, and a whole bunch of things while working at `Imaginary
|
||||
Landscape`_). Me starting a project like this makes sense if it's
|
||||
done in Python.
|
||||
|
||||
You might say that PHP is way more deployable, that Rails has way
|
||||
more cool developers riding around on fixie bikes---and all of
|
||||
those things are true. But I know Python, like Python, and think
|
||||
that Python is pretty great. I do think that deployment in Python
|
||||
is not as good as with PHP, but I think the days of shared hosting
|
||||
are (thankfully) coming to an end, and will probably be replaced
|
||||
by cheap virtual machines spun up on the fly for people who want
|
||||
that sort of stuff, and Python will be a huge part of that future,
|
||||
maybe even more than PHP will. The deployment tools are getting
|
||||
better. Maybe we can use something like Silver Lining. Maybe we
|
||||
can just distribute as ``.debs`` or ``.rpms``. We'll figure it
|
||||
out when we get there.
|
||||
|
||||
Regardless, if I'm starting this project, which I am, it's gonna
|
||||
be in Python.
|
||||
|
||||
.. _Miro Community: http://mirocommunity.org/
|
||||
.. _Miro Guide: http://miroguide.org/
|
||||
.. _Creative Commons: http://creativecommons.org/
|
||||
.. _Imaginary Landscape: http://www.imagescape.com/
|
||||
|
||||
|
||||
Why WSGI Minimalism
|
||||
===================
|
||||
|
||||
Chris Webber on "Why WSGI Minimalism":
|
||||
|
||||
If you notice in the technology list I list a lot of components
|
||||
that are very "django-like", but not actually `Django`_
|
||||
components. What can I say, I really like a lot of the ideas in
|
||||
Django! Which leads to the question: why not just use Django?
|
||||
|
||||
While I really like Django's ideas and a lot of its components, I
|
||||
also feel that most of the best ideas in Django I want have been
|
||||
implemented as good or even better outside of Django. I could
|
||||
just use Django and replace the templating system with Jinja2, and
|
||||
the form system with wtforms, and the database with MongoDB and
|
||||
MongoKit, but at that point, how much of Django is really left?
|
||||
|
||||
I also am sometimes saddened and irritated by how coupled all of
|
||||
Django's components are. Loosely coupled yes, but still coupled.
|
||||
WSGI has done a good job of providing a base layer for running
|
||||
applications on and if you know how to do it yourself [1]_, it's
|
||||
not hard or many lines of code at all to bind them together
|
||||
without any framework at all (not even say `Pylons`_, `Pyramid`_
|
||||
or `Flask`_ which I think are still great projects, especially for
|
||||
people who want this sort of thing but have no idea how to get
|
||||
started). And even at this already really early stage of writing
|
||||
MediaGoblin, that glue work is mostly done.
|
||||
|
||||
Not to say I don't think Django isn't great for a lot of things.
|
||||
For a lot of stuff, it's still the best, but not for MediaGoblin,
|
||||
I think.
|
||||
|
||||
One thing that Django does super well though is documentation. It
|
||||
still has some faults, but even with those considered I can hardly
|
||||
think of any other project in Python that has as nice of
|
||||
documentation as Django. It may be worth learning some lessons on
|
||||
documentation from Django [2]_, on that note.
|
||||
|
||||
I'd really like to have a good, thorough hacking-howto and
|
||||
deployment-howto, especially in the former making some notes on
|
||||
how to make it easier for Django hackers to get started.
|
||||
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
.. _Pylons: http://pylonshq.com/
|
||||
.. _Pyramid: http://docs.pylonsproject.org/projects/pyramid/dev/
|
||||
.. _Flask: http://flask.pocoo.org/
|
||||
|
||||
.. [1] http://pythonpaste.org/webob/do-it-yourself.html
|
||||
.. [2] http://pycon.blip.tv/file/4881071/
|
||||
|
||||
|
||||
Why MongoDB
|
||||
===========
|
||||
|
||||
(Note: We don't use MongoDB anymore. This is the original rationale,
|
||||
however.)
|
||||
|
||||
Chris Webber on "Why MongoDB":
|
||||
|
||||
In case you were wondering, I am not a NOSQL fanboy, I do not go
|
||||
around telling people that MongoDB is web scale. Actually my
|
||||
choice for MongoDB isn't scalability, though scaling up really
|
||||
nicely is a pretty good feature and sets us up well in case large
|
||||
volume sites eventually do use MediaGoblin. But there's another
|
||||
side of scalability, and that's scaling down, which is important
|
||||
for federation, maybe even more important than scaling up in an
|
||||
ideal universe where everyone ran servers out of their own
|
||||
housing. As a memory-mapped database, MongoDB is pretty hungry,
|
||||
so actually I spent a lot of time debating whether the inability
|
||||
to scale down as nicely as something like SQL has with sqlite
|
||||
meant that it was out.
|
||||
|
||||
But I decided in the end that I really want MongoDB, not for
|
||||
scalability, but for flexibility. Schema evolution pains in SQL
|
||||
are almost enough reason for me to want MongoDB, but not quite.
|
||||
The real reason is because I want the ability to eventually handle
|
||||
multiple media types through MediaGoblin, and also allow for
|
||||
plugins, without the rigidity of tables making that difficult. In
|
||||
other words, something like::
|
||||
|
||||
{"title": "Me talking until you are bored",
|
||||
"description": "blah blah blah",
|
||||
"media_type": "audio",
|
||||
"media_data": {
|
||||
"length": "2:30",
|
||||
"codec": "OGG Vorbis"},
|
||||
"plugin_data": {
|
||||
"licensing": {
|
||||
"license": "http://creativecommons.org/licenses/by-sa/3.0/"}}}
|
||||
|
||||
|
||||
Being able to just dump media-specific information in a media_data
|
||||
hashtable is pretty great, and even better is having a plugin
|
||||
system where you can just let plugins have their own entire
|
||||
key-value space cleanly inside the document that doesn't interfere
|
||||
with anyone else's stuff. If we were to let plugins to deposit
|
||||
their own information inside the database, either we'd let plugins
|
||||
create their own tables which makes SQL migrations even harder
|
||||
than they already are, or we'd probably end up creating a table
|
||||
with a column for key, a column for value, and a column for type
|
||||
in one huge table called "plugin_data" or something similar. (Yo
|
||||
dawg, I heard you liked plugins, so I put a database in your
|
||||
database so you can query while you query.) Gross.
|
||||
|
||||
I also don't want things to be too loose so that we forget or lose
|
||||
the structure of things, and that's one reason why I want to use
|
||||
MongoKit, because we can cleanly define a much structure as we
|
||||
want and verify that documents match that structure generally
|
||||
without adding too much bloat or overhead (MongoKit is a pretty
|
||||
lightweight wrapper and doesn't inject extra MongoKit-specific
|
||||
stuff into the database, which is nice and nicer than many other
|
||||
ORMs in that way).
|
||||
|
||||
|
||||
Why Sphinx for documentation
|
||||
============================
|
||||
|
||||
Will Kahn-Greene on "Why Sphinx":
|
||||
|
||||
`Sphinx`_ is a fantastic tool for organizing documentation for a
|
||||
Python-based project that makes it pretty easy to write docs that
|
||||
are readable in source form and can be "compiled" into HTML, LaTeX
|
||||
and other formats.
|
||||
|
||||
There are other doc systems out there, but given that GNU
|
||||
MediaGoblin is being written in Python and I've done a ton of
|
||||
documentation using Sphinx, it makes sense to use Sphinx for now.
|
||||
|
||||
.. _Sphinx: http://sphinx.pocoo.org/
|
||||
|
||||
|
||||
Why AGPLv3 and CC0?
|
||||
===================
|
||||
|
||||
Chris, Brett, Will, Rob, Matt, et al curated into a story where
|
||||
everyone is the hero by Will on "Why AGPLv3 and CC0":
|
||||
|
||||
The `AGPL v3`_ preserves the freedoms guaranteed by the GPL v3 in
|
||||
the context of software as a service. Using this license ensures
|
||||
that users of the service have the ability to examine the source,
|
||||
deploy their own instance, and implement their own version. This
|
||||
is really important to us and a core mission component of this
|
||||
project. Thus we decided that the software parts should be under
|
||||
this license.
|
||||
|
||||
However, the project is made up of more than just software:
|
||||
there's CSS, images, and other output-related things. We wanted
|
||||
the templates/images/css side of the project all permissive and
|
||||
permissive in the same absolutely permissive way. We're waiving
|
||||
our copyrights to non-software things under the CC0 waiver.
|
||||
|
||||
That brings us to the templates where there's some code and some
|
||||
output. The template engine we're using is called Jinja2. It
|
||||
mixes HTML markup with Python code to render the output of the
|
||||
software. We decided the templates are part of the output of the
|
||||
software and not the software itself. We wanted the output of the
|
||||
software to be licensed in a hassle-free way so that when someone
|
||||
deploys their own GNU MediaGoblin instance with their own
|
||||
templates, they don't have to deal with the copyleft aspects of
|
||||
the AGPLv3 and we'd be fine with that because the changes they're
|
||||
making are identity-related. So at first we decided to waive our
|
||||
copyrights to the templates with a CC0 waiver and then add an
|
||||
exception to the AGPLv3 for the software such that the templates
|
||||
can make calls into the software and yet be a separately licensed
|
||||
work. However, Brett brought up the question of whether this
|
||||
allows some unscrupulous person to make changes to the software
|
||||
through the templates in such a way that they're not bound by the
|
||||
AGPLv3: i.e. a loophole. We thought about this loophole and
|
||||
between this and the extra legalese involved in the exception to
|
||||
the AGPLv3, we decided that it's just way simpler if the templates
|
||||
were also licensed under the AGPLv3.
|
||||
|
||||
Then we have the licensing for the documentation. Given that the
|
||||
documentation is tied to the software content-wise, we don't feel
|
||||
like we have to worry about ensuring freedom of the documentation
|
||||
or worry about attribution concerns. Thus we're waiving our
|
||||
copyrights to the documentation under CC0 as well.
|
||||
|
||||
Lastly, we have branding. This covers logos and other things that
|
||||
are distinctive to GNU MediaGoblin that we feel represents this
|
||||
project. Since we don't currently have any branding, this is an
|
||||
open issue, but we're thinking we'll go with a CC BY-SA license.
|
||||
|
||||
By licensing in this way, we make sure that users of the software
|
||||
receive the freedoms that the AGPLv3 ensures regardless of what
|
||||
fate befalls this project.
|
||||
|
||||
So to summarize:
|
||||
|
||||
* software (Python, JavaScript, HTML templates): licensed
|
||||
under AGPLv3
|
||||
* non-software things (CSS, images, video): copyrights waived
|
||||
under CC0 because this is output of the software
|
||||
* documentation: copyrights waived under CC0 because it's not part
|
||||
of the software
|
||||
* branding assets: we're kicking this can down the road, but
|
||||
probably CC BY-SA
|
||||
|
||||
This is all codified in the ``COPYING`` file.
|
||||
|
||||
.. _AGPL v3: http://www.gnu.org/licenses/agpl.html
|
||||
.. _CC0 v1: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
|
||||
Why (non-mandatory) copyright assignment?
|
||||
=========================================
|
||||
|
||||
Chris Webber on "Why copyright assignment?":
|
||||
|
||||
GNU MediaGoblin is a GNU project with non-mandatory but heavily
|
||||
encouraged copyright assignment to the FSF. Most, if not all, of
|
||||
the core contributors to GNU MediaGoblin will have done a
|
||||
copyright assignment, but unlike some other GNU projects, it isn't
|
||||
required here. We think this is the best choice for GNU
|
||||
MediaGoblin: it ensures that the Free Software Foundation may
|
||||
protect the software by enforcing the AGPL if the FSF sees fit,
|
||||
but it also means that we can immediately merge in changes from a
|
||||
new contributor. It also means that some significant non-FSF
|
||||
contributors might also be able to enforce the AGPL if seen fit.
|
||||
|
||||
Again, assignment is not mandatory, but it is heavily encouraged,
|
||||
even incentivized: significant contributors who do a copyright
|
||||
assignment to the FSF are eligible to have a unique goblin drawing
|
||||
produced for them by the project's main founder, Christopher Allan
|
||||
Webber. See `the wiki <http://wiki.mediagoblin.org/>`_ for details.
|
||||
|
||||
|
125
docs/source/devel/storage.rst
Normal file
125
docs/source/devel/storage.rst
Normal file
@ -0,0 +1,125 @@
|
||||
=========
|
||||
Storage
|
||||
=========
|
||||
|
||||
The storage systems attached to your app
|
||||
----------------------------------------
|
||||
|
||||
Dynamic content: queue_store and public_store
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Two instances of the StorageInterface come attached to your app. These
|
||||
are:
|
||||
|
||||
+ **queue_store:** When a user submits a fresh piece of media for
|
||||
their gallery, before the Processing stage, that piece of media sits
|
||||
here in the queue_store. (It's possible that we'll rename this to
|
||||
"private_store" and start storing more non-publicly-stored stuff in
|
||||
the future...). This is a StorageInterface implementation
|
||||
instance. Visitors to your site probably cannot see it... it isn't
|
||||
designed to be seen, anyway.
|
||||
|
||||
+ **public_store:** After your media goes through processing it gets
|
||||
moved to the public store. This is also a StorageInterface
|
||||
implelementation, and is for stuff that's intended to be seen by
|
||||
site visitors.
|
||||
|
||||
The workbench
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
In addition, there's a "workbench" used during
|
||||
processing... it's just for temporary files during
|
||||
processing, and also for making local copies of stuff that
|
||||
might be on remote storage interfaces while transitionally
|
||||
moving/converting from the queue_store to the public store.
|
||||
See the workbench module documentation for more.
|
||||
|
||||
.. automodule:: mediagoblin.tools.workbench
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Static assets / staticdirect
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
On top of all that, there is some static media that comes bundled with your
|
||||
application. This stuff is kept in:
|
||||
|
||||
mediagoblin/static/
|
||||
|
||||
These files are for mediagoblin base assets. Things like the CSS files,
|
||||
logos, etc. You can mount these at whatever location is appropriate to you
|
||||
(see the direct_remote_path option in the config file) so if your users
|
||||
are keeping their static assets at http://static.mgoblin.example.org/ but
|
||||
their actual site is at http://mgoblin.example.org/, you need to be able
|
||||
to get your static files in a where-it's-mounted agnostic way. There's a
|
||||
"staticdirector" attached to the request object. It's pretty easy to use;
|
||||
just look at this bit taken from the
|
||||
mediagoblin/templates/mediagoblin/base.html main template:
|
||||
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="Template:Request.staticdirect('/css/extlib/text.css')"/>
|
||||
|
||||
see? Not too hard. As expected, if you configured direct_remote_path to be
|
||||
http://static.mgoblin.example.org/ you'll get back
|
||||
http://static.mgoblin.example.org/css/extlib/text.css just as you'd
|
||||
probably expect.
|
||||
|
||||
StorageInterface and implementations
|
||||
------------------------------------
|
||||
|
||||
The guts of StorageInterface and friends
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
So, the StorageInterface!
|
||||
|
||||
So, the public and queue stores both use StorageInterface implementations
|
||||
... but what does that mean? It's not too hard.
|
||||
|
||||
Open up:
|
||||
|
||||
mediagoblin/storage.py
|
||||
|
||||
In here you'll see a couple of things. First of all, there's the
|
||||
StorageInterface class. What you'll see is that this is just a very simple
|
||||
python class. A few of the methods actually implement things, but for the
|
||||
most part, they don't. What really matters about this class is the
|
||||
docstrings. Each expected method is documented as to how it should be
|
||||
constructed. Want to make a new StorageInterface? Simply subclass it. Want
|
||||
to know how to use the methods of your storage system? Read these docs,
|
||||
they span all implementations.
|
||||
|
||||
There are a couple of implementations of these classes bundled in
|
||||
storage.py as well. The most simple of these is BasicFileStorage, which is
|
||||
also the default storage system used. As expected, this stores files
|
||||
locally on your machine.
|
||||
|
||||
There's also a CloudFileStorage system. This provides a mapping to
|
||||
[OpenStack's swift http://swift.openstack.org/] storage system (used by
|
||||
RackSpace Cloud files and etc).
|
||||
|
||||
Between these two examples you should be able to get a pretty good idea of
|
||||
how to write your own storage systems, for storing data across your
|
||||
beowulf cluster of radioactive monkey brains, whatever.
|
||||
|
||||
Writing code to store stuff
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
So what does coding for StorageInterface implementations actually look
|
||||
like? It's pretty simple, really. For one thing, the design is fairly
|
||||
inspired by [Django's file storage API
|
||||
https://docs.djangoproject.com/en/dev/ref/files/storage/]... with some
|
||||
differences.
|
||||
|
||||
Basically, you access files on "file paths", which aren't exactly like
|
||||
unix file paths, but are close. If you wanted to store a file on a path
|
||||
like dir1/dir2/filename.jpg you'd actually write that file path like:
|
||||
|
||||
['dir1', 'dir2', 'filename.jpg']
|
||||
|
||||
This way we can be *sure* that each component is actually a component of
|
||||
the path that's expected... we do some filename cleaning on each component.
|
||||
|
||||
Your StorageInterface should pass in and out "file like objects". In other
|
||||
words, they should provide .read() and .write() at minimum, and probably
|
||||
also .seek() and .close().
|
@ -44,7 +44,6 @@ MediaGoblin website. It is written for site administrators.
|
||||
siteadmin/relnotes
|
||||
siteadmin/theming
|
||||
siteadmin/plugins
|
||||
siteadmin/codebase
|
||||
|
||||
|
||||
.. _core-plugin-section:
|
||||
@ -58,6 +57,8 @@ Part 2: Core plugin documentation
|
||||
plugindocs/flatpagesfile
|
||||
plugindocs/sampleplugin
|
||||
plugindocs/oauth
|
||||
plugindocs/trim_whitespace
|
||||
plugindocs/raven
|
||||
|
||||
|
||||
Part 3: Plugin Writer's Guide
|
||||
@ -70,6 +71,21 @@ This guide covers writing new GNU MediaGoblin plugins.
|
||||
|
||||
pluginwriter/foreward
|
||||
pluginwriter/quickstart
|
||||
pluginwriter/database
|
||||
pluginwriter/api
|
||||
|
||||
|
||||
Part 4: Developer's Zone
|
||||
========================
|
||||
|
||||
This chapter contains various information for developers.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
devel/codebase
|
||||
devel/storage
|
||||
devel/originaldesigndecisions
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
2
docs/source/plugindocs/raven.rst
Normal file
2
docs/source/plugindocs/raven.rst
Normal file
@ -0,0 +1,2 @@
|
||||
.. _raven-setup: Set up the raven plugin
|
||||
.. include:: ../../../mediagoblin/plugins/raven/README.rst
|
1
docs/source/plugindocs/trim_whitespace.rst
Normal file
1
docs/source/plugindocs/trim_whitespace.rst
Normal file
@ -0,0 +1 @@
|
||||
.. include:: ../../../mediagoblin/plugins/trim_whitespace/README.rst
|
127
docs/source/pluginwriter/api.rst
Normal file
127
docs/source/pluginwriter/api.rst
Normal file
@ -0,0 +1,127 @@
|
||||
.. MediaGoblin Documentation
|
||||
|
||||
Written in 2013 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/>.
|
||||
|
||||
|
||||
==========
|
||||
Plugin API
|
||||
==========
|
||||
|
||||
This documents the general plugin API.
|
||||
|
||||
Please note, at this point OUR PLUGIN HOOKS MAY AND WILL CHANGE.
|
||||
Authors are encouraged to develop plugins and work with the
|
||||
MediaGoblin community to keep them up to date, but this API will be a
|
||||
moving target for a few releases.
|
||||
|
||||
Please check the release notes for updates!
|
||||
|
||||
:mod:`pluginapi` Module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: mediagoblin.tools.pluginapi
|
||||
:members: get_config, register_routes, register_template_path,
|
||||
register_template_hooks, get_hook_templates,
|
||||
hook_handle, hook_runall, hook_transform
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Your plugin may define its own configuration defaults.
|
||||
|
||||
Simply add to the directory of your plugin a config_spec.ini file. An
|
||||
example might look like::
|
||||
|
||||
[plugin_spec]
|
||||
some_string = string(default="blork")
|
||||
some_int = integer(default=50)
|
||||
|
||||
This means that when people enable your plugin in their config you'll
|
||||
be able to provide defaults as well as type validation.
|
||||
|
||||
|
||||
Context Hooks
|
||||
-------------
|
||||
|
||||
View specific hooks
|
||||
+++++++++++++++++++
|
||||
|
||||
You can hook up to almost any template called by any specific view
|
||||
fairly easily. As long as the view directly or indirectly uses the
|
||||
method ``render_to_response`` you can access the context via a hook
|
||||
that has a key in the format of the tuple::
|
||||
|
||||
(view_symbolic_name, view_template_path)
|
||||
|
||||
Where the "view symbolic name" is the same parameter used in
|
||||
``request.urlgen()`` to look up the view. So say we're wanting to add
|
||||
something to the context of the user's homepage. We look in
|
||||
mediagoblin/user_pages/routing.py and see::
|
||||
|
||||
add_route('mediagoblin.user_pages.user_home',
|
||||
'/u/<string:user>/',
|
||||
'mediagoblin.user_pages.views:user_home')
|
||||
|
||||
Aha! That means that the name is ``mediagoblin.user_pages.user_home``.
|
||||
Okay, so then we look at the view at the
|
||||
``mediagoblin.user_pages.user_home`` method::
|
||||
|
||||
@uses_pagination
|
||||
def user_home(request, page):
|
||||
# [...] whole bunch of stuff here
|
||||
return render_to_response(
|
||||
request,
|
||||
'mediagoblin/user_pages/user.html',
|
||||
{'user': user,
|
||||
'user_gallery_url': user_gallery_url,
|
||||
'media_entries': media_entries,
|
||||
'pagination': pagination})
|
||||
|
||||
Nice! So the template appears to be
|
||||
``mediagoblin/user_pages/user.html``. Cool, that means that the key
|
||||
is::
|
||||
|
||||
("mediagoblin.user_pages.user_home",
|
||||
"mediagoblin/user_pages/user.html")
|
||||
|
||||
The context hook uses ``hook_transform()`` so that means that if we're
|
||||
hooking into it, our hook will both accept one argument, ``context``,
|
||||
and should return that modified object, like so::
|
||||
|
||||
def add_to_user_home_context(context):
|
||||
context['foo'] = 'bar'
|
||||
return context
|
||||
|
||||
hooks = {
|
||||
("mediagoblin.user_pages.user_home",
|
||||
"mediagoblin/user_pages/user.html"): add_to_user_home_context}
|
||||
|
||||
|
||||
Global context hooks
|
||||
++++++++++++++++++++
|
||||
|
||||
If you need to add something to the context of *every* view, it is not
|
||||
hard; there are two hooks hook that also uses hook_transform (like the
|
||||
above) but make available what you are providing to *every* view.
|
||||
|
||||
Note that there is a slight, but critical, difference between the two.
|
||||
|
||||
The most general one is the ``'template_global_context'`` hook. This
|
||||
one is run only once, and is read into the global context... all views
|
||||
will get access to what are in this dict.
|
||||
|
||||
The slightly more expensive but more powerful one is
|
||||
``'template_context_prerender'``. This one is not added to the global
|
||||
context... it is added to the actual context of each individual
|
||||
template render right before it is run! Because of this you also can
|
||||
do some powerful and crazy things, such as checking the request object
|
||||
or other parts of the context before passing them on.
|
111
docs/source/pluginwriter/database.rst
Normal file
111
docs/source/pluginwriter/database.rst
Normal file
@ -0,0 +1,111 @@
|
||||
.. MediaGoblin Documentation
|
||||
|
||||
Written in 2013 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/>.
|
||||
|
||||
|
||||
========
|
||||
Database
|
||||
========
|
||||
|
||||
|
||||
Accessing Existing Data
|
||||
=======================
|
||||
|
||||
If your plugin wants to access existing data, this is quite
|
||||
straight forward. Just import the appropiate models and use
|
||||
the full power of SQLAlchemy. Take a look at the (upcoming)
|
||||
database section in the Developer's Chapter.
|
||||
|
||||
|
||||
Creating new Tables
|
||||
===================
|
||||
|
||||
If your plugin needs some new space to store data, you
|
||||
should create a new table. Please do not modify core
|
||||
tables. Not doing so might seem inefficient and possibly
|
||||
is. It will help keep things sane and easier to upgrade
|
||||
versions later.
|
||||
|
||||
So if you create a new plugin and need new tables, create a
|
||||
file named ``models.py`` in your plugin directory. You
|
||||
might take a look at the core's db.models for some ideas.
|
||||
Here's a simple one:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from mediagoblin.db.base import Base
|
||||
from sqlalchemy import Column, Integer, Unicode, ForeignKey
|
||||
|
||||
class MediaSecurity(Base):
|
||||
__tablename__ = "yourplugin__media_security"
|
||||
|
||||
# The primary key *and* reference to the main media_entry
|
||||
media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
|
||||
primary_key=True)
|
||||
get_media_entry = relationship("MediaEntry",
|
||||
backref=backref("security_rating", cascade="all, delete-orphan"))
|
||||
|
||||
rating = Column(Unicode)
|
||||
|
||||
MODELS = [MediaSecurity]
|
||||
|
||||
That's it.
|
||||
|
||||
Some notes:
|
||||
|
||||
* Make sure all your ``__tablename__`` start with your
|
||||
plugin's name so the tables of various plugins can't
|
||||
conflict in the database. (Conflicts in python naming are
|
||||
much easier to fix later).
|
||||
* Try to get your database design as good as possible in
|
||||
the first attempt. Changing the database design later,
|
||||
when people already have data using the old design, is
|
||||
possible (see next chapter), but it's not easy.
|
||||
|
||||
|
||||
Changing the Database Schema Later
|
||||
==================================
|
||||
|
||||
If your plugin is in use and instances use it to store some
|
||||
data, changing the database design is a tricky thing.
|
||||
|
||||
1. Make up your mind how the new schema should look like.
|
||||
2. Change ``models.py`` to contain the new schema. Keep a
|
||||
copy of the old version around for your personal
|
||||
reference later.
|
||||
3. Now make up your mind (possibly using your old and new
|
||||
``models.py``) what steps in SQL are needed to convert
|
||||
the old schema to the new one.
|
||||
This is called a "migration".
|
||||
4. Create a file ``migrations.py`` that will contain all
|
||||
your migrations and add your new migration.
|
||||
|
||||
Take a look at the core's ``db/migrations.py`` for some
|
||||
good examples on what you might be able to do. Here's a
|
||||
simple one to add one column:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
|
||||
from sqlalchemy import MetaData, Column, Integer
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
@RegisterMigration(1, MIGRATIONS)
|
||||
def add_license_preference(db):
|
||||
metadata = MetaData(bind=db.bind)
|
||||
|
||||
security_table = inspect_table(metadata, 'yourplugin__media_security')
|
||||
|
||||
col = Column('security_level', Integer)
|
||||
col.create(security_table)
|
||||
db.commit()
|
@ -32,6 +32,11 @@ GNU/Linux distro.
|
||||
install. If instead you want to join in as a contributor, see our
|
||||
`Hacking HOWTO <http://wiki.mediagoblin.org/HackingHowto>`_ instead.
|
||||
|
||||
There are also many ways to install servers... for the sake of
|
||||
simplicity, our instructions below describe installing with nginx.
|
||||
For more recipes, including Apache, see
|
||||
`our wiki <http://wiki.mediagoblin.org/Deployment>`_.
|
||||
|
||||
Prepare System
|
||||
--------------
|
||||
|
||||
@ -165,7 +170,7 @@ And set up the in-package virtualenv::
|
||||
|
||||
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 man need to
|
||||
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.
|
||||
|
||||
@ -173,22 +178,50 @@ 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
|
||||
you're familiar with Python packaging you may consider deploying with
|
||||
your preferred the method.
|
||||
your preferred method.
|
||||
|
||||
Assuming you are going to deploy with FastCGI, you should also install
|
||||
flup::
|
||||
|
||||
./bin/easy_install flup
|
||||
|
||||
(Sometimes this breaks because flup's site is flakey. If it does for
|
||||
you, try)::
|
||||
|
||||
./bin/easy_install https://pypi.python.org/pypi/flup/1.0.3.dev-20110405
|
||||
|
||||
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
|
||||
|
||||
Note: If you are running an active site, depending on your server
|
||||
configuration, you may need to stop it first or the dbupdate command
|
||||
may hang (and it's certainly a good idea to restart it after the
|
||||
update)
|
||||
|
||||
|
||||
Deploy MediaGoblin Services
|
||||
---------------------------
|
||||
|
||||
Edit site configuration
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A few basic properties must be set before MediaGoblin will work. First
|
||||
make a copy of ``mediagoblin.ini`` for editing so the original config
|
||||
file isn't lost::
|
||||
|
||||
cp mediagoblin.ini mediagoblin_local.ini
|
||||
|
||||
Then:
|
||||
- Set ``email_sender_address`` to the address you wish to be used as
|
||||
the sender for system-generated emails
|
||||
- Edit ``direct_remote_path``, ``base_dir``, and ``base_url`` if
|
||||
your mediagoblin directory is not the root directory of your
|
||||
vhost.
|
||||
|
||||
|
||||
Configure MediaGoblin to use the PostgreSQL database
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -224,11 +257,11 @@ browser to confirm that the service is operable.
|
||||
|
||||
.. _webserver-config:
|
||||
|
||||
Connect the Webserver to MediaGoblin with FastCGI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This section describes how to configure MediaGoblin to work via
|
||||
FastCGI. Our configuration example will use nginx, however, you may
|
||||
FastCGI and nginx
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration example will use nginx, however, you may
|
||||
use any webserver of your choice as long as it supports the FastCGI
|
||||
protocol. If you do not already have a web server, consider nginx, as
|
||||
the configuration files may be more clear than the
|
||||
@ -271,6 +304,10 @@ this ``nginx.conf`` file should be modeled on the following::
|
||||
# Change this to update the upload size limit for your users
|
||||
client_max_body_size 8m;
|
||||
|
||||
# prevent attacks (someone uploading a .txt file that the browser
|
||||
# interprets as an HTML file, etc.)
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
server_name mediagoblin.example.org www.mediagoblin.example.org;
|
||||
access_log /var/log/nginx/mediagoblin.example.access.log;
|
||||
error_log /var/log/nginx/mediagoblin.example.error.log;
|
||||
@ -325,3 +362,24 @@ Visit the site you've set up in your browser by visiting
|
||||
smaller deployments. However, for larger production deployments
|
||||
with larger processing requirements, see the
|
||||
":doc:`production-deployments`" documentation.
|
||||
|
||||
|
||||
Apache
|
||||
~~~~~~
|
||||
|
||||
Instructions and scripts for running MediaGoblin on an Apache server
|
||||
can be found on the `MediaGoblin wiki <http://wiki.mediagoblin.org/Deployment>`_.
|
||||
|
||||
|
||||
Security Considerations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. warning::
|
||||
|
||||
The directory ``user_dev/crypto/`` contains some very
|
||||
sensitive files.
|
||||
Especially the ``itsdangeroussecret.bin`` is very important
|
||||
for session security. Make sure not to leak its contents anywhere.
|
||||
If the contents gets leaked nevertheless, delete your file
|
||||
and restart the server, so that it creates a new secret key.
|
||||
All previous sessions will be invalifated then.
|
||||
|
@ -43,6 +43,15 @@ video media types, then the list would look like this::
|
||||
|
||||
media_types = mediagoblin.media_types.image, mediagoblin.media_types.video
|
||||
|
||||
Note that after enabling new media types, you must run dbupdate like so::
|
||||
|
||||
./bin/gmg dbupdate
|
||||
|
||||
If you are running an active site, depending on your server
|
||||
configuration, you may need to stop it first (and it's certainly a
|
||||
good idea to restart it after the update).
|
||||
|
||||
|
||||
How does MediaGoblin decide which media type to use for a file?
|
||||
===============================================================
|
||||
|
||||
@ -62,12 +71,27 @@ Video
|
||||
|
||||
To enable video, first install gstreamer and the python-gstreamer
|
||||
bindings (as well as whatever gstremaer extensions you want,
|
||||
good/bad/ugly). On Debianoid systems::
|
||||
good/bad/ugly). On Debianoid systems
|
||||
|
||||
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-{base,bad,good,ugly} \
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python-gst0.10 \
|
||||
gstreamer0.10-plugins-base \
|
||||
gstreamer0.10-plugins-bad \
|
||||
gstreamer0.10-plugins-good \
|
||||
gstreamer0.10-plugins-ugly \
|
||||
gstreamer0.10-ffmpeg
|
||||
|
||||
|
||||
Add ``mediagoblin.media_types.video`` to the ``media_types`` list in your
|
||||
``mediagoblin_local.ini`` and restart MediaGoblin.
|
||||
|
||||
Run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/gmg dbupdate
|
||||
|
||||
Now you should be able to submit videos, and mediagoblin should
|
||||
transcode them.
|
||||
|
||||
@ -92,7 +116,9 @@ To install these on Debianoid systems, run::
|
||||
|
||||
The ``scikits.audiolab`` package you will install in the next step depends on the
|
||||
``libsndfile1-dev`` package, so we should install it.
|
||||
On Debianoid systems, run::
|
||||
On Debianoid systems, run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install libsndfile1-dev
|
||||
|
||||
@ -108,8 +134,15 @@ Then install ``scikits.audiolab`` for the spectrograms::
|
||||
./bin/pip install scikits.audiolab
|
||||
|
||||
Add ``mediagoblin.media_types.audio`` to the ``media_types`` list in your
|
||||
``mediagoblin_local.ini`` and restart MediaGoblin. You should now be able to
|
||||
upload and listen to audio files!
|
||||
``mediagoblin_local.ini`` and restart MediaGoblin.
|
||||
|
||||
Run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/gmg dbupdate
|
||||
|
||||
You should now be able to upload and listen to audio files!
|
||||
|
||||
|
||||
Ascii art
|
||||
@ -117,7 +150,9 @@ Ascii art
|
||||
|
||||
To enable ascii art support, first install the
|
||||
`chardet <http://pypi.python.org/pypi/chardet>`_
|
||||
library, which is necessary for creating thumbnails of ascii art::
|
||||
library, which is necessary for creating thumbnails of ascii art
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/easy_install chardet
|
||||
|
||||
@ -131,4 +166,69 @@ the list would look like this::
|
||||
|
||||
media_types = mediagoblin.media_types.image, mediagoblin.media_types.ascii
|
||||
|
||||
Run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/gmg dbupdate
|
||||
|
||||
Now any .txt file you uploaded will be processed as ascii art!
|
||||
|
||||
|
||||
STL / 3d model support
|
||||
======================
|
||||
|
||||
To enable the "STL" 3d model support plugin, first make sure you have
|
||||
a recentish `Blender <http://blender.org>`_ installed and available on
|
||||
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
|
||||
``mediagoblin_local.ini`` and restart MediaGoblin.
|
||||
|
||||
Run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/gmg dbupdate
|
||||
|
||||
You should now be able to upload .obj and .stl files and MediaGoblin
|
||||
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 install this on Fedora:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo yum install -y poppler-utils unoconv libreoffice-headless
|
||||
|
||||
pdf.js relies on git submodules, so be sure you have fetched them:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
This feature has been tested on Fedora with:
|
||||
poppler-utils-0.20.2-9.fc18.x86_64
|
||||
unoconv-0.5-2.fc18.noarch
|
||||
libreoffice-headless-3.6.5.2-8.fc18.x86_64
|
||||
|
||||
It may work on some earlier versions, but that is not guaranteed.
|
||||
|
||||
Add ``mediagoblin.media_types.pdf`` to the ``media_types`` list in your
|
||||
``mediagoblin_local.ini`` and restart MediaGoblin.
|
||||
|
||||
Run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/gmg dbupdate
|
||||
|
||||
|
||||
|
@ -44,29 +44,33 @@ If the plugin is available on the `Python Package Index
|
||||
pip install <plugin-name>
|
||||
|
||||
For example, if we wanted to install the plugin named
|
||||
"mediagoblin-restrictfive", we would do::
|
||||
"mediagoblin-licenses" (which allows you to customize the licenses you
|
||||
offer for your media), we would do::
|
||||
|
||||
pip install mediagoblin-restrictfive
|
||||
pip install mediagoblin-licenses
|
||||
|
||||
.. Note::
|
||||
|
||||
If you're using a virtual environment, make sure to activate the
|
||||
virtual environment before installing with pip. Otherwise the
|
||||
plugin may get installed in a different environment than the one
|
||||
MediaGoblin is installed in.
|
||||
virtual environment before installing with pip. Otherwise the plugin
|
||||
may get installed in a different environment than the one MediaGoblin
|
||||
is installed in. Also make sure, you use e.g. pip-2.7 if your default
|
||||
python (and thus pip) is python 3 (e.g. in Ubuntu).
|
||||
|
||||
Once you've installed the plugin software, you need to tell
|
||||
MediaGoblin that this is a plugin you want MediaGoblin to use. To do
|
||||
that, you edit the ``mediagoblin.ini`` file and add the plugin as a
|
||||
subsection of the plugin section.
|
||||
|
||||
For example, say the "mediagoblin-restrictfive" plugin had the Python
|
||||
package path ``restrictfive``, then you would add ``restrictfive`` to
|
||||
For example, say the "mediagoblin-licenses" plugin has the Python
|
||||
package path ``mediagoblin_licenses``, then you would add ``mediagoblin_licenses`` to
|
||||
the ``plugins`` section as a subsection::
|
||||
|
||||
[plugins]
|
||||
|
||||
[[restrictfive]]
|
||||
[[mediagoblin_licenses]]
|
||||
license_01=abbrev1, name1, http://url1
|
||||
license_02=abbrev2, name1, http://url2
|
||||
|
||||
|
||||
Configuring plugins
|
||||
@ -112,7 +116,7 @@ Removing plugins
|
||||
|
||||
To remove a plugin, use ``pip uninstall``. For example::
|
||||
|
||||
pip uninstall mediagoblin-restrictfive
|
||||
pip uninstall mediagoblin-licenses
|
||||
|
||||
.. Note::
|
||||
|
||||
|
@ -52,7 +52,7 @@ as the basis for your script: ::
|
||||
Separate Celery
|
||||
---------------
|
||||
|
||||
While the ``./lazyserer.sh`` configuration provides an efficient way to
|
||||
While the ``./lazyserver.sh`` configuration provides an efficient way to
|
||||
start using a MediaGoblin instance, it is not suitable for production
|
||||
deployments for several reasons:
|
||||
|
||||
@ -77,6 +77,17 @@ Modify your existing MediaGoblin and application init scripts, if
|
||||
necessary, to prevent them from starting their own ``celeryd``
|
||||
processes.
|
||||
|
||||
.. _sentry:
|
||||
|
||||
Set up sentry to monitor exceptions
|
||||
-----------------------------------
|
||||
|
||||
We have a plugin for `raven`_ integration, see the ":doc:`/plugindocs/raven`"
|
||||
documentation.
|
||||
|
||||
.. _`raven`: http://raven.readthedocs.org
|
||||
|
||||
|
||||
.. _init-script:
|
||||
|
||||
Use an Init Script
|
||||
|
@ -19,6 +19,166 @@ 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.3.3
|
||||
=====
|
||||
|
||||
**Do this to upgrade**
|
||||
|
||||
1. Make sure to run ``bin/gmg dbupdate`` after upgrading.
|
||||
2. OpenStreetMap is now a plugin, so if you want to use it, add the
|
||||
following to your config file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[plugins]
|
||||
[[mediagoblin.plugins.geolocation]]
|
||||
|
||||
If you have your own theme, you may need to make some adjustments to
|
||||
it as some theme related things may have changed in this release. If
|
||||
you run into problems, don't hesitate to
|
||||
`contact us <http://mediagoblin.org/pages/join.html>`_
|
||||
(IRC is often best).
|
||||
|
||||
**New features**
|
||||
|
||||
* New dropdown menu for accessing various features.
|
||||
|
||||
* Significantly improved URL generation. Now mediagoblin won't give
|
||||
up on making a slug if it looks like there will be a duplicate;
|
||||
it'll try extra hard to generate a meaningful one instead.
|
||||
|
||||
Similarly, linking to an id no longer can possibly conflict with
|
||||
linking to a slug; /u/username/m/id:35/ is the kind of reference we
|
||||
now use to linking to entries with ids. However, old links with
|
||||
entries that linked to ids should work just fine with our migration.
|
||||
The only urls that might break in this release are ones using colons
|
||||
or equal signs.
|
||||
|
||||
* New template hooks for plugin authoring.
|
||||
|
||||
* As a demonstration of new template hooks for plugin authoring,
|
||||
openstreetmap support now moved to a plugin!
|
||||
|
||||
* Method to add media to collections switched from icon of paperclip
|
||||
to button with "add to collection" text.
|
||||
|
||||
* Bug where videos often failed to produce a proper thumbnail fixed!
|
||||
|
||||
* Copying around files in MediaGoblin now much more efficient, doesn't
|
||||
waste gobs of memory.
|
||||
|
||||
* Video transcoding now optional for videos that meet certain
|
||||
criteria. By default, MediaGoblin will not transcode webm videos
|
||||
that are smaller in resolution than the MediaGoblin defaults, and
|
||||
MediaGoblin can also be configured to allow theora files to not be
|
||||
transcoded as well.
|
||||
|
||||
* Per-user license preference option; always want your uploads to be
|
||||
BY-SA and tired of changing that field? You can now set your
|
||||
license preference in your user settings.
|
||||
|
||||
* Video player now responsive; better for mobile!
|
||||
|
||||
* You can now delete your account from the user preferences page if
|
||||
you so wish.
|
||||
|
||||
**Other changes**
|
||||
|
||||
* Plugin writers: Internal restructuring led to mediagoblin.db.sql* be
|
||||
mediagoblin.db.* starting from 0.3.3
|
||||
|
||||
* Dependency list has been reduced not requiring the "webob" package anymore.
|
||||
|
||||
* And many small fixes/improvements, too numerous to list!
|
||||
|
||||
|
||||
0.3.2
|
||||
=====
|
||||
|
||||
This will be the last release that is capable of converting from an earlier
|
||||
MongoDB-based MediaGoblin instance to the newer SQL-based system.
|
||||
|
||||
**Do this to upgrade**
|
||||
|
||||
# directory of your mediagoblin install
|
||||
cd /srv/mediagoblin.example.org
|
||||
|
||||
# copy source for this release
|
||||
git fetch
|
||||
git checkout tags/v0.3.2
|
||||
|
||||
# perform any needed database updates
|
||||
bin/gmg dbupdate
|
||||
|
||||
# restart your servers however you do that, e.g.,
|
||||
sudo service mediagoblin-paster restart
|
||||
sudo service mediagoblin-celeryd restart
|
||||
|
||||
|
||||
**New features**
|
||||
|
||||
* **3d model support!**
|
||||
|
||||
You can now upload STL and OBJ files and display them in
|
||||
MediaGoblin. Requires a recent-ish Blender; for details see:
|
||||
:ref:`deploying-chapter`
|
||||
|
||||
* **trim_whitespace**
|
||||
|
||||
We bundle the optional plugin trim_whitespace which reduces the size
|
||||
of the delivered html output by reducing redundant whitespace.
|
||||
|
||||
See :ref:`core-plugin-section` for plugin documentation
|
||||
|
||||
* **A new API!**
|
||||
|
||||
It isn't well documented yet but we do have an API. There is an
|
||||
`android application in progress <https://gitorious.org/mediagoblin/mediagoblin-android>`_
|
||||
which makes use of it, and there are some demo applications between
|
||||
`automgtic <https://github.com/jwandborg/automgtic>`_, an
|
||||
automatic media uploader for your desktop
|
||||
and `OMGMG <https://github.com/jwandborg/omgmg>`_, an example of
|
||||
a web application hooking up to the API.
|
||||
|
||||
This is a plugin, so you have to enable it in your mediagoblin
|
||||
config file by adding a section under [plugins] like::
|
||||
|
||||
[plugins]
|
||||
[[mediagoblin.plugins.api]]
|
||||
|
||||
Note that the API works but is not nailed down... the way it is
|
||||
called may change in future releases.
|
||||
|
||||
* **OAuth login support**
|
||||
|
||||
For applications that use OAuth to connect to the API.
|
||||
|
||||
This is a plugin, so you have to enable it in your mediagoblin
|
||||
config file by adding a section under [plugins] like::
|
||||
|
||||
[plugins]
|
||||
[[mediagoblin.plugins.oauth]]
|
||||
|
||||
* **Collections**
|
||||
|
||||
We now have user-curated collections support. These are arbitrary
|
||||
galleries that are customizable by users. You can add media to
|
||||
these by clicking on the paperclip icon when logged in and looking
|
||||
at a media entry.
|
||||
|
||||
* **OpenStreetMap licensing display improvements**
|
||||
|
||||
More accurate display of OSM licensing, and less disruptive: you
|
||||
click to "expand" the display of said licensing.
|
||||
|
||||
Geolocation is also now on by default.
|
||||
|
||||
* **Miscelaneous visual improvements**
|
||||
|
||||
We've made a number of small visual improvements including newer and
|
||||
nicer looking thumbnails and improved checkbox placement.
|
||||
|
||||
|
||||
|
||||
0.3.1
|
||||
=====
|
||||
|
@ -17,7 +17,7 @@ unwittingly interfere with other software that depends on the
|
||||
canonical release versions of those same libraries!
|
||||
|
||||
Forking upstream software for trivial reasons makes us bad citizens in
|
||||
the Open Source community and adds unnecessary heartache for our
|
||||
the Free Software community and adds unnecessary heartache for our
|
||||
users. Don't make us "that" project.
|
||||
|
||||
|
||||
@ -63,6 +63,19 @@ FAQ
|
||||
This is a last resort; consult with the rest of the dev group
|
||||
before taking this radical step.
|
||||
|
||||
:Q: What about submodules?
|
||||
|
||||
:A: pdf.js is supplied as a submodule, and other software may use that too,
|
||||
to add a new submodule:
|
||||
git submodule add <git-repo-of-fun-project> extlib/fun-project
|
||||
|
||||
Use it just like a snapshotted extlib directory. When a new clone of mediagoblin
|
||||
is made you need to run
|
||||
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
As noted in HackingHowto
|
||||
|
||||
Thanks
|
||||
======
|
||||
|
@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Library to extract EXIF information from digital camera image files
|
||||
# http://sourceforge.net/projects/exif-py/
|
||||
#
|
||||
# Library to extract EXIF information from digital camera image files.
|
||||
# https://github.com/ianare/exif-py
|
||||
#
|
||||
#
|
||||
# VERSION 1.1.0
|
||||
#
|
||||
@ -22,7 +24,6 @@
|
||||
#
|
||||
# These 2 are useful when you are retrieving a large list of images
|
||||
#
|
||||
#
|
||||
# To return an error on invalid tags,
|
||||
# pass the -s or --strict argument, or as
|
||||
# tags = EXIF.process_file(f, strict=True)
|
||||
@ -48,7 +49,7 @@
|
||||
# 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'
|
||||
#
|
||||
# Copyright (c) 2002-2007 Gene Cash All rights reserved
|
||||
# Copyright (c) 2007-2008 Ianaré Sévi All rights reserved
|
||||
# Copyright (c) 2007-2012 Ianaré Sévi All rights reserved
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
@ -102,7 +103,7 @@ def make_string_uc(seq):
|
||||
seq = seq[8:]
|
||||
# Of course, this is only correct if ASCII, and the standard explicitly
|
||||
# allows JIS and Unicode.
|
||||
return make_string(seq)
|
||||
return make_string( make_string(seq) )
|
||||
|
||||
# field type descriptions as (length, abbreviation, full name) tuples
|
||||
FIELD_TYPES = (
|
||||
@ -171,9 +172,9 @@ EXIF_TAGS = {
|
||||
3: 'Rotated 180',
|
||||
4: 'Mirrored vertical',
|
||||
5: 'Mirrored horizontal then rotated 90 CCW',
|
||||
6: 'Rotated 90 CW',
|
||||
6: 'Rotated 90 CCW',
|
||||
7: 'Mirrored horizontal then rotated 90 CW',
|
||||
8: 'Rotated 90 CCW'}),
|
||||
8: 'Rotated 90 CW'}),
|
||||
0x0115: ('SamplesPerPixel', ),
|
||||
0x0116: ('RowsPerStrip', ),
|
||||
0x0117: ('StripByteCounts', ),
|
||||
@ -251,40 +252,54 @@ EXIF_TAGS = {
|
||||
2: 'CenterWeightedAverage',
|
||||
3: 'Spot',
|
||||
4: 'MultiSpot',
|
||||
5: 'Pattern'}),
|
||||
5: 'Pattern',
|
||||
6: 'Partial',
|
||||
255: 'other'}),
|
||||
0x9208: ('LightSource',
|
||||
{0: 'Unknown',
|
||||
1: 'Daylight',
|
||||
2: 'Fluorescent',
|
||||
3: 'Tungsten',
|
||||
9: 'Fine Weather',
|
||||
10: 'Flash',
|
||||
3: 'Tungsten (incandescent light)',
|
||||
4: 'Flash',
|
||||
9: 'Fine weather',
|
||||
10: 'Cloudy weather',
|
||||
11: 'Shade',
|
||||
12: 'Daylight Fluorescent',
|
||||
13: 'Day White Fluorescent',
|
||||
14: 'Cool White Fluorescent',
|
||||
15: 'White Fluorescent',
|
||||
17: 'Standard Light A',
|
||||
18: 'Standard Light B',
|
||||
19: 'Standard Light C',
|
||||
12: 'Daylight fluorescent (D 5700 - 7100K)',
|
||||
13: 'Day white fluorescent (N 4600 - 5400K)',
|
||||
14: 'Cool white fluorescent (W 3900 - 4500K)',
|
||||
15: 'White fluorescent (WW 3200 - 3700K)',
|
||||
17: 'Standard light A',
|
||||
18: 'Standard light B',
|
||||
19: 'Standard light C',
|
||||
20: 'D55',
|
||||
21: 'D65',
|
||||
22: 'D75',
|
||||
255: 'Other'}),
|
||||
23: 'D50',
|
||||
24: 'ISO studio tungsten',
|
||||
255: 'other light source',}),
|
||||
0x9209: ('Flash',
|
||||
{0: 'No',
|
||||
1: 'Fired',
|
||||
5: 'Fired (?)', # no return sensed
|
||||
7: 'Fired (!)', # return sensed
|
||||
9: 'Fill Fired',
|
||||
13: 'Fill Fired (?)',
|
||||
15: 'Fill Fired (!)',
|
||||
16: 'Off',
|
||||
24: 'Auto Off',
|
||||
25: 'Auto Fired',
|
||||
29: 'Auto Fired (?)',
|
||||
31: 'Auto Fired (!)',
|
||||
32: 'Not Available'}),
|
||||
{0: 'Flash did not fire',
|
||||
1: 'Flash fired',
|
||||
5: 'Strobe return light not detected',
|
||||
7: 'Strobe return light detected',
|
||||
9: 'Flash fired, compulsory flash mode',
|
||||
13: 'Flash fired, compulsory flash mode, return light not detected',
|
||||
15: 'Flash fired, compulsory flash mode, return light detected',
|
||||
16: 'Flash did not fire, compulsory flash mode',
|
||||
24: 'Flash did not fire, auto mode',
|
||||
25: 'Flash fired, auto mode',
|
||||
29: 'Flash fired, auto mode, return light not detected',
|
||||
31: 'Flash fired, auto mode, return light detected',
|
||||
32: 'No flash function',
|
||||
65: 'Flash fired, red-eye reduction mode',
|
||||
69: 'Flash fired, red-eye reduction mode, return light not detected',
|
||||
71: 'Flash fired, red-eye reduction mode, return light detected',
|
||||
73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
|
||||
77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
|
||||
79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
|
||||
89: 'Flash fired, auto mode, red-eye reduction mode',
|
||||
93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
|
||||
95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'}),
|
||||
0x920A: ('FocalLength', ),
|
||||
0x9214: ('SubjectArea', ),
|
||||
0x927C: ('MakerNote', ),
|
||||
@ -410,7 +425,10 @@ GPS_TAGS = {
|
||||
0x0018: ('GPSDestBearing', ),
|
||||
0x0019: ('GPSDestDistanceRef', ),
|
||||
0x001A: ('GPSDestDistance', ),
|
||||
0x001B: ('GPSProcessingMethod', ),
|
||||
0x001C: ('GPSAreaInformation', ),
|
||||
0x001D: ('GPSDate', ),
|
||||
0x001E: ('GPSDifferential', ),
|
||||
}
|
||||
|
||||
# Ignore these tags when quick processing
|
||||
@ -1231,10 +1249,17 @@ class IFD_Tag:
|
||||
return self.printable
|
||||
|
||||
def __repr__(self):
|
||||
return '(0x%04X) %s=%s @ %d' % (self.tag,
|
||||
try:
|
||||
s= '(0x%04X) %s=%s @ %d' % (self.tag,
|
||||
FIELD_TYPES[self.field_type][2],
|
||||
self.printable,
|
||||
self.field_offset)
|
||||
except:
|
||||
s= '(%s) %s=%s @ %s' % (str(self.tag),
|
||||
FIELD_TYPES[self.field_type][2],
|
||||
self.printable,
|
||||
str(self.field_offset))
|
||||
return s
|
||||
|
||||
# class that handles an EXIF header
|
||||
class EXIF_header:
|
||||
@ -1283,7 +1308,11 @@ class EXIF_header:
|
||||
# return pointer to next IFD
|
||||
def next_IFD(self, ifd):
|
||||
entries=self.s2n(ifd, 2)
|
||||
return self.s2n(ifd+2+12*entries, 4)
|
||||
next_ifd = self.s2n(ifd+2+12*entries, 4)
|
||||
if next_ifd == ifd:
|
||||
return 0
|
||||
else:
|
||||
return next_ifd
|
||||
|
||||
# return list of IFDs in header
|
||||
def list_IFDs(self):
|
||||
@ -1348,14 +1377,15 @@ class EXIF_header:
|
||||
# special case: null-terminated ASCII string
|
||||
# XXX investigate
|
||||
# sometimes gets too big to fit in int value
|
||||
if count != 0 and count < (2**31):
|
||||
self.file.seek(self.offset + offset)
|
||||
values = self.file.read(count)
|
||||
#print values
|
||||
# Drop any garbage after a null.
|
||||
values = values.split('\x00', 1)[0]
|
||||
else:
|
||||
values = ''
|
||||
if count != 0: # and count < (2**31): # 2E31 is hardware dependant. --gd
|
||||
try:
|
||||
self.file.seek(self.offset + offset)
|
||||
values = self.file.read(count)
|
||||
#print values
|
||||
# Drop any garbage after a null.
|
||||
values = values.split('\x00', 1)[0]
|
||||
except OverflowError:
|
||||
values = ''
|
||||
else:
|
||||
values = []
|
||||
signed = (field_type in [6, 8, 9, 10])
|
||||
@ -1567,7 +1597,8 @@ class EXIF_header:
|
||||
dict=MAKERNOTE_CANON_TAGS)
|
||||
for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
|
||||
('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
|
||||
self.canon_decode_tag(self.tags[i[0]].values, i[1])
|
||||
if i[0] in self.tags:
|
||||
self.canon_decode_tag(self.tags[i[0]].values, i[1])
|
||||
return
|
||||
|
||||
|
||||
@ -1613,26 +1644,124 @@ def process_file(f, stop_tag='UNDEF', details=True, strict=False, debug=False):
|
||||
offset = 0
|
||||
elif data[0:2] == '\xFF\xD8':
|
||||
# it's a JPEG file
|
||||
if debug: print "JPEG format recognized data[0:2] == '0xFFD8'."
|
||||
base = 2
|
||||
while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
|
||||
if debug: print "data[2] == 0xxFF data[3]==%x and data[6:10] = %s"%(ord(data[3]),data[6:10])
|
||||
length = ord(data[4])*256+ord(data[5])
|
||||
if debug: print "Length offset is",length
|
||||
f.read(length-8)
|
||||
# fake an EXIF beginning of file
|
||||
# I don't think this is used. --gd
|
||||
data = '\xFF\x00'+f.read(10)
|
||||
fake_exif = 1
|
||||
if data[2] == '\xFF' and data[6:10] == 'Exif':
|
||||
if base>2:
|
||||
if debug: print "added to base "
|
||||
base = base + length + 4 -2
|
||||
else:
|
||||
if debug: print "added to zero "
|
||||
base = length + 4
|
||||
if debug: print "Set segment base to",base
|
||||
|
||||
# Big ugly patch to deal with APP2 (or other) data coming before APP1
|
||||
f.seek(0)
|
||||
data = f.read(base+4000) # in theory, this could be insufficient since 64K is the maximum size--gd
|
||||
# base = 2
|
||||
while 1:
|
||||
if debug: print "Segment base 0x%X" % base
|
||||
if data[base:base+2]=='\xFF\xE1':
|
||||
# APP1
|
||||
if debug: print "APP1 at base",hex(base)
|
||||
if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
|
||||
if debug: print "Code",data[base+4:base+8]
|
||||
if data[base+4:base+8] == "Exif":
|
||||
if debug: print "Decrement base by",2,"to get to pre-segment header (for compatibility with later code)"
|
||||
base = base-2
|
||||
break
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
elif data[base:base+2]=='\xFF\xE0':
|
||||
# APP0
|
||||
if debug: print "APP0 at base",hex(base)
|
||||
if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
|
||||
if debug: print "Code",data[base+4:base+8]
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
elif data[base:base+2]=='\xFF\xE2':
|
||||
# APP2
|
||||
if debug: print "APP2 at base",hex(base)
|
||||
if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
|
||||
if debug: print "Code",data[base+4:base+8]
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
elif data[base:base+2]=='\xFF\xEE':
|
||||
# APP14
|
||||
if debug: print "APP14 Adobe segment at base",hex(base)
|
||||
if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
|
||||
if debug: print "Code",data[base+4:base+8]
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
print "There is useful EXIF-like data here, but we have no parser for it."
|
||||
base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
elif data[base:base+2]=='\xFF\xDB':
|
||||
if debug: print "JPEG image data at base",hex(base),"No more segments are expected."
|
||||
# sys.exit(0)
|
||||
break
|
||||
elif data[base:base+2]=='\xFF\xD8':
|
||||
# APP12
|
||||
if debug: print "FFD8 segment at base",hex(base)
|
||||
if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead."
|
||||
if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
|
||||
if debug: print "Code",data[base+4:base+8]
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
elif data[base:base+2]=='\xFF\xEC':
|
||||
# APP12
|
||||
if debug: print "APP12 XMP (Ducky) or Pictureinfo segment at base",hex(base)
|
||||
if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead."
|
||||
if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
|
||||
if debug: print "Code",data[base+4:base+8]
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
print "There is useful EXIF-like data here (quality, comment, copyright), but we have no parser for it."
|
||||
base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
else:
|
||||
try:
|
||||
if debug: print "Unexpected/unhandled segment type or file content."
|
||||
if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead."
|
||||
if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
|
||||
except: pass
|
||||
try: base=base+ord(data[base+2])*256+ord(data[base+3])+2
|
||||
except: pass
|
||||
|
||||
f.seek(base+12)
|
||||
if data[2+base] == '\xFF' and data[6+base:10+base] == 'Exif':
|
||||
# detected EXIF header
|
||||
offset = f.tell()
|
||||
endian = f.read(1)
|
||||
#HACK TEST: endian = 'M'
|
||||
elif data[2+base] == '\xFF' and data[6+base:10+base+1] == 'Ducky':
|
||||
# detected Ducky header.
|
||||
if debug: print "EXIF-like header (normally 0xFF and code):",hex(ord(data[2+base])) , "and", data[6+base:10+base+1]
|
||||
offset = f.tell()
|
||||
endian = f.read(1)
|
||||
elif data[2+base] == '\xFF' and data[6+base:10+base+1] == 'Adobe':
|
||||
# detected APP14 (Adobe)
|
||||
if debug: print "EXIF-like header (normally 0xFF and code):",hex(ord(data[2+base])) , "and", data[6+base:10+base+1]
|
||||
offset = f.tell()
|
||||
endian = f.read(1)
|
||||
else:
|
||||
# no EXIF information
|
||||
if debug: print "No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)"
|
||||
if debug: print " but got",hex(ord(data[2+base])) , "and", data[6+base:10+base+1]
|
||||
return {}
|
||||
else:
|
||||
# file format not recognized
|
||||
if debug: print "file format not recognized"
|
||||
return {}
|
||||
|
||||
# deal with the EXIF info we found
|
||||
if debug:
|
||||
print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
|
||||
print "Endian format is ",endian
|
||||
print {'I': 'Intel', 'M': 'Motorola', '\x01':'Adobe Ducky', 'd':'XMP/Adobe unknown' }[endian], 'format'
|
||||
hdr = EXIF_header(f, endian, offset, fake_exif, strict, debug)
|
||||
ifd_list = hdr.list_IFDs()
|
||||
ctr = 0
|
||||
|
131
extlib/exif/changes.txt
Normal file
131
extlib/exif/changes.txt
Normal file
@ -0,0 +1,131 @@
|
||||
~ EXIF.py Changelog ~
|
||||
|
||||
2012-11-30 - Gregory Dudek (date of merge).
|
||||
Patches and changes:
|
||||
Overflow error fixes added (related to 2**31 size)
|
||||
GPS tags added.
|
||||
|
||||
2012-09-26 - Ianaré Sévi
|
||||
Merge patches:
|
||||
Add GPS tags
|
||||
Add better endian debug info
|
||||
|
||||
2012-06-13 - Ianaré Sévi
|
||||
Merge patches:
|
||||
Support malformed last IFD by fhats
|
||||
Light source, Flash and Metering mode dictionaries update by gryfik
|
||||
|
||||
2008-07-31 - Ianaré Sévi
|
||||
Wikipedia Commons hunt for suitable test case images,
|
||||
testing new code additions.
|
||||
|
||||
2008-07-09 - Stephen H. Olson
|
||||
Fix a problem with reading MakerNotes out of NEF files.
|
||||
Add some more Nikon MakerNote tags.
|
||||
|
||||
2008-07-08 - Stephen H. Olson
|
||||
An error check for large tags totally borked MakerNotes.
|
||||
With Nikon anyway, valid MakerNotes can be pretty big.
|
||||
Add error check for a crash caused by nikon_ev_bias being
|
||||
called with the wrong args.
|
||||
Drop any garbage after a null character in string
|
||||
(patch from Andrew McNabb <amcnabb@google.com>).
|
||||
|
||||
2008-02-12 - Ianaré Sévi
|
||||
Fix crash on invalid MakerNote
|
||||
Fix crash on huge Makernote (temp fix)
|
||||
Add printIM tag 0xC4A5, needs decoding info
|
||||
Add 0x9C9B-F range of tags
|
||||
Add a bunch of tag definitions from:
|
||||
http://owl.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html
|
||||
Add 'strict' variable and command line option
|
||||
|
||||
2008-01-18 - Gunter Ohrner
|
||||
Add 'GPSDate' tag
|
||||
|
||||
2007-12-12 - Ianaré Sévi
|
||||
Fix quick option on certain image types
|
||||
Add note on tag naming in documentation
|
||||
|
||||
2007-11-30 - Ianaré Sévi
|
||||
Changed -s option to -t
|
||||
Put changelog into separate file
|
||||
|
||||
2007-10-28 - Ianaré Sévi
|
||||
Merged changes from MoinMoin:ReimarBauer
|
||||
Added command line option for debug, stop
|
||||
processing on tag.
|
||||
|
||||
2007-09-27 - Ianaré Sévi
|
||||
Add some Olympus Makernote tags.
|
||||
|
||||
2007-09-26 - Stephen H. Olson
|
||||
Don't error out on invalid Olympus 'SpecialMode'.
|
||||
Add a few more Olympus/Minolta tags.
|
||||
|
||||
2007-09-22 - Stephen H. Olson
|
||||
Don't error on invalid string
|
||||
Improved Nikon MakerNote support
|
||||
|
||||
2007-05-03 - Martin Stone <mj_stone@users.sourceforge.net>
|
||||
Fix for inverted detailed flag and Photoshop header
|
||||
|
||||
2007-03-24 - Ianaré Sévi
|
||||
Can now ignore MakerNotes Tags for faster processing.
|
||||
|
||||
2007-01-18 - Ianaré Sévi <ianare@gmail.com>
|
||||
Fixed a couple errors and assuming maintenance of the library.
|
||||
|
||||
2006-08-04 MoinMoin:ReimarBauer
|
||||
Added an optional parameter name to process_file and dump_IFD. Using this parameter the
|
||||
loop is breaked after that tag_name is processed.
|
||||
some PEP8 changes
|
||||
|
||||
---------------------------- original notices -------------------------
|
||||
|
||||
Contains code from "exifdump.py" originally written by Thierry Bousch
|
||||
<bousch@topo.math.u-psud.fr> and released into the public domain.
|
||||
|
||||
Updated and turned into general-purpose library by Gene Cash
|
||||
|
||||
Patch Contributors:
|
||||
* Simon J. Gerraty <sjg@crufty.net>
|
||||
s2n fix & orientation decode
|
||||
* John T. Riedl <riedl@cs.umn.edu>
|
||||
Added support for newer Nikon type 3 Makernote format for D70 and some
|
||||
other Nikon cameras.
|
||||
* Joerg Schaefer <schaeferj@gmx.net>
|
||||
Fixed subtle bug when faking an EXIF header, which affected maker notes
|
||||
using relative offsets, and a fix for Nikon D100.
|
||||
|
||||
1999-08-21 TB Last update by Thierry Bousch to his code.
|
||||
|
||||
2002-01-17 CEC Discovered code on web.
|
||||
Commented everything.
|
||||
Made small code improvements.
|
||||
Reformatted for readability.
|
||||
|
||||
2002-01-19 CEC Added ability to read TIFFs and JFIF-format JPEGs.
|
||||
Added ability to extract JPEG formatted thumbnail.
|
||||
Added ability to read GPS IFD (not tested).
|
||||
Converted IFD data structure to dictionaries indexed by
|
||||
tag name.
|
||||
Factored into library returning dictionary of IFDs plus
|
||||
thumbnail, if any.
|
||||
|
||||
2002-01-20 CEC Added MakerNote processing logic.
|
||||
Added Olympus MakerNote.
|
||||
Converted data structure to single-level dictionary, avoiding
|
||||
tag name collisions by prefixing with IFD name. This makes
|
||||
it much easier to use.
|
||||
2002-01-23 CEC Trimmed nulls from end of string values.
|
||||
|
||||
2002-01-25 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote.
|
||||
|
||||
2002-01-26 CEC Added ability to extract TIFF thumbnails.
|
||||
Added Nikon, Fujifilm, Casio MakerNotes.
|
||||
|
||||
2003-11-30 CEC Fixed problem with canon_decode_tag() not creating an
|
||||
IFD_Tag() object.
|
||||
|
||||
2004-02-15 CEC Finally fixed bit shift warning by converting Y to 0L.
|
@ -20,7 +20,7 @@
|
||||
# Bram de Jong <bram.dejong at domain.com where domain in gmail>
|
||||
# 2012, Joar Wandborg <first name at last name dot se>
|
||||
|
||||
import Image, ImageDraw, ImageColor #@UnresolvedImport
|
||||
from PIL import Image, ImageDraw, ImageColor #@UnresolvedImport
|
||||
from functools import partial
|
||||
import math
|
||||
import numpy
|
||||
|
@ -1,20 +0,0 @@
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
3
extlib/html5shiv/html5shiv.js
vendored
3
extlib/html5shiv/html5shiv.js
vendored
@ -1,3 +0,0 @@
|
||||
// HTML5 Shiv v3 | @jon_neal @afarkas @rem | MIT/GPL2 Licensed
|
||||
// Uncompressed source: https://github.com/aFarkas/html5shiv
|
||||
(function(a,b){var c=function(a){return a.innerHTML="<x-element></x-element>",a.childNodes.length===1}(b.createElement("a")),d=function(a,b,c){return b.appendChild(a),(c=(c?c(a):a.currentStyle).display)&&b.removeChild(a)&&c==="block"}(b.createElement("nav"),b.documentElement,a.getComputedStyle),e={elements:"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),shivDocument:function(a){a=a||b;if(a.documentShived)return;a.documentShived=!0;var f=a.createElement,g=a.createDocumentFragment,h=a.getElementsByTagName("head")[0],i=function(a){f(a)};c||(e.elements.join(" ").replace(/\w+/g,i),a.createElement=function(a){var b=f(a);return b.canHaveChildren&&e.shivDocument(b.document),b},a.createDocumentFragment=function(){return e.shivDocument(g())});if(!d&&h){var j=f("div");j.innerHTML=["x<style>","article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}","audio{display:none}","canvas,video{display:inline-block;*display:inline;*zoom:1}","[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}","mark{background:#FF0;color:#000}","</style>"].join(""),h.insertBefore(j.lastChild,h.firstChild)}return a}};e.shivDocument(b),a.html5=e})(this,document)
|
@ -1,4 +1,5 @@
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyright 2013 jQuery Foundation and other contributors
|
||||
http://jquery.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
1
extlib/pdf.js
Submodule
1
extlib/pdf.js
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 369b81b63f560b5d729da26752ca541503d81510
|
165
extlib/thingiview.js/LICENSE
Normal file
165
extlib/thingiview.js/LICENSE
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
202
extlib/thingiview.js/Three.js
Normal file
202
extlib/thingiview.js/Three.js
Normal file
@ -0,0 +1,202 @@
|
||||
// Three.js r32 - http://github.com/mrdoob/three.js
|
||||
var THREE=THREE||{};THREE.Color=function(a){this.autoUpdate=true;this.setHex(a)};
|
||||
THREE.Color.prototype={setRGB:function(a,c,d){this.r=a;this.g=c;this.b=d;if(this.autoUpdate){this.updateHex();this.updateStyleString()}},setHex:function(a){this.hex=~~a&16777215;if(this.autoUpdate){this.updateRGBA();this.updateStyleString()}},updateHex:function(){this.hex=~~(this.r*255)<<16^~~(this.g*255)<<8^~~(this.b*255)},updateRGBA:function(){this.r=(this.hex>>16&255)/255;this.g=(this.hex>>8&255)/255;this.b=(this.hex&255)/255},updateStyleString:function(){this.__styleString="rgb("+~~(this.r*255)+
|
||||
","+~~(this.g*255)+","+~~(this.b*255)+")"},clone:function(){return new THREE.Color(this.hex)},toString:function(){return"THREE.Color ( r: "+this.r+", g: "+this.g+", b: "+this.b+", hex: "+this.hex+" )"}};THREE.Vector2=function(a,c){this.x=a||0;this.y=c||0};
|
||||
THREE.Vector2.prototype={set:function(a,c){this.x=a;this.y=c;return this},copy:function(a){this.x=a.x;this.y=a.y;return this},addSelf:function(a){this.x+=a.x;this.y+=a.y;return this},add:function(a,c){this.x=a.x+c.x;this.y=a.y+c.y;return this},subSelf:function(a){this.x-=a.x;this.y-=a.y;return this},sub:function(a,c){this.x=a.x-c.x;this.y=a.y-c.y;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;return this},unit:function(){this.multiplyScalar(1/this.length());return this},length:function(){return Math.sqrt(this.x*
|
||||
this.x+this.y*this.y)},lengthSq:function(){return this.x*this.x+this.y*this.y},negate:function(){this.x=-this.x;this.y=-this.y;return this},clone:function(){return new THREE.Vector2(this.x,this.y)},toString:function(){return"THREE.Vector2 ("+this.x+", "+this.y+")"}};THREE.Vector3=function(a,c,d){this.x=a||0;this.y=c||0;this.z=d||0};
|
||||
THREE.Vector3.prototype={set:function(a,c,d){this.x=a;this.y=c;this.z=d;return this},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;return this},add:function(a,c){this.x=a.x+c.x;this.y=a.y+c.y;this.z=a.z+c.z;return this},addSelf:function(a){this.x+=a.x;this.y+=a.y;this.z+=a.z;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;return this},sub:function(a,c){this.x=a.x-c.x;this.y=a.y-c.y;this.z=a.z-c.z;return this},subSelf:function(a){this.x-=a.x;this.y-=a.y;this.z-=a.z;return this},
|
||||
cross:function(a,c){this.x=a.y*c.z-a.z*c.y;this.y=a.z*c.x-a.x*c.z;this.z=a.x*c.y-a.y*c.x;return this},crossSelf:function(a){var c=this.x,d=this.y,e=this.z;this.x=d*a.z-e*a.y;this.y=e*a.x-c*a.z;this.z=c*a.y-d*a.x;return this},multiply:function(a,c){this.x=a.x*c.x;this.y=a.y*c.y;this.z=a.z*c.z;return this},multiplySelf:function(a){this.x*=a.x;this.y*=a.y;this.z*=a.z;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;return this},divideSelf:function(a){this.x/=a.x;this.y/=a.y;this.z/=
|
||||
a.z;return this},divideScalar:function(a){this.x/=a;this.y/=a;this.z/=a;return this},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z},distanceTo:function(a){var c=this.x-a.x,d=this.y-a.y;a=this.z-a.z;return Math.sqrt(c*c+d*d+a*a)},distanceToSquared:function(a){var c=this.x-a.x,d=this.y-a.y;a=this.z-a.z;return c*c+d*d+a*a},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z},negate:function(){this.x=
|
||||
-this.x;this.y=-this.y;this.z=-this.z;return this},normalize:function(){var a=Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z);a>0?this.multiplyScalar(1/a):this.set(0,0,0);return this},setLength:function(a){return this.normalize().multiplyScalar(a)},isZero:function(){return Math.abs(this.x)<1.0E-4&&Math.abs(this.y)<1.0E-4&&Math.abs(this.z)<1.0E-4},clone:function(){return new THREE.Vector3(this.x,this.y,this.z)},toString:function(){return"THREE.Vector3 ( "+this.x+", "+this.y+", "+this.z+" )"}};
|
||||
THREE.Vector4=function(a,c,d,e){this.x=a||0;this.y=c||0;this.z=d||0;this.w=e||1};
|
||||
THREE.Vector4.prototype={set:function(a,c,d,e){this.x=a;this.y=c;this.z=d;this.w=e;return this},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;this.w=a.w||1;return this},add:function(a,c){this.x=a.x+c.x;this.y=a.y+c.y;this.z=a.z+c.z;this.w=a.w+c.w;return this},addSelf:function(a){this.x+=a.x;this.y+=a.y;this.z+=a.z;this.w+=a.w;return this},sub:function(a,c){this.x=a.x-c.x;this.y=a.y-c.y;this.z=a.z-c.z;this.w=a.w-c.w;return this},subSelf:function(a){this.x-=a.x;this.y-=a.y;this.z-=a.z;this.w-=a.w;
|
||||
return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;this.w*=a;return this},divideScalar:function(a){this.x/=a;this.y/=a;this.z/=a;this.w/=a;return this},lerpSelf:function(a,c){this.x+=(a.x-this.x)*c;this.y+=(a.y-this.y)*c;this.z+=(a.z-this.z)*c;this.w+=(a.w-this.w)*c},clone:function(){return new THREE.Vector4(this.x,this.y,this.z,this.w)},toString:function(){return"THREE.Vector4 ("+this.x+", "+this.y+", "+this.z+", "+this.w+")"}};
|
||||
THREE.Ray=function(a,c){this.origin=a||new THREE.Vector3;this.direction=c||new THREE.Vector3};
|
||||
THREE.Ray.prototype={intersectScene:function(a){var c,d,e=a.objects,g=[];a=0;for(c=e.length;a<c;a++){d=e[a];if(d instanceof THREE.Mesh)g=g.concat(this.intersectObject(d))}g.sort(function(h,o){return h.distance-o.distance});return g},intersectObject:function(a){function c(K,p,U,F){F=F.clone().subSelf(p);U=U.clone().subSelf(p);var f=K.clone().subSelf(p);K=F.dot(F);p=F.dot(U);F=F.dot(f);var j=U.dot(U);U=U.dot(f);f=1/(K*j-p*p);j=(j*F-p*U)*f;K=(K*U-p*F)*f;return j>0&&K>0&&j+K<1}var d,e,g,h,o,b,i,k,y,z,
|
||||
u,x=a.geometry,H=x.vertices,J=[];d=0;for(e=x.faces.length;d<e;d++){g=x.faces[d];z=this.origin.clone();u=this.direction.clone();h=a.matrix.multiplyVector3(H[g.a].position.clone());o=a.matrix.multiplyVector3(H[g.b].position.clone());b=a.matrix.multiplyVector3(H[g.c].position.clone());i=g instanceof THREE.Face4?a.matrix.multiplyVector3(H[g.d].position.clone()):null;k=a.rotationMatrix.multiplyVector3(g.normal.clone());y=u.dot(k);if(y<0){k=k.dot((new THREE.Vector3).sub(h,z))/y;z=z.addSelf(u.multiplyScalar(k));
|
||||
if(g instanceof THREE.Face3){if(c(z,h,o,b)){g={distance:this.origin.distanceTo(z),point:z,face:g,object:a};J.push(g)}}else if(g instanceof THREE.Face4)if(c(z,h,o,i)||c(z,o,b,i)){g={distance:this.origin.distanceTo(z),point:z,face:g,object:a};J.push(g)}}}return J}};
|
||||
THREE.Rectangle=function(){function a(){h=e-c;o=g-d}var c,d,e,g,h,o,b=true;this.getX=function(){return c};this.getY=function(){return d};this.getWidth=function(){return h};this.getHeight=function(){return o};this.getLeft=function(){return c};this.getTop=function(){return d};this.getRight=function(){return e};this.getBottom=function(){return g};this.set=function(i,k,y,z){b=false;c=i;d=k;e=y;g=z;a()};this.addPoint=function(i,k){if(b){b=false;c=i;d=k;e=i;g=k}else{c=c<i?c:i;d=d<k?d:k;e=e>i?e:i;g=g>k?
|
||||
g:k}a()};this.add3Points=function(i,k,y,z,u,x){if(b){b=false;c=i<y?i<u?i:u:y<u?y:u;d=k<z?k<x?k:x:z<x?z:x;e=i>y?i>u?i:u:y>u?y:u;g=k>z?k>x?k:x:z>x?z:x}else{c=i<y?i<u?i<c?i:c:u<c?u:c:y<u?y<c?y:c:u<c?u:c;d=k<z?k<x?k<d?k:d:x<d?x:d:z<x?z<d?z:d:x<d?x:d;e=i>y?i>u?i>e?i:e:u>e?u:e:y>u?y>e?y:e:u>e?u:e;g=k>z?k>x?k>g?k:g:x>g?x:g:z>x?z>g?z:g:x>g?x:g}a()};this.addRectangle=function(i){if(b){b=false;c=i.getLeft();d=i.getTop();e=i.getRight();g=i.getBottom()}else{c=c<i.getLeft()?c:i.getLeft();d=d<i.getTop()?d:i.getTop();
|
||||
e=e>i.getRight()?e:i.getRight();g=g>i.getBottom()?g:i.getBottom()}a()};this.inflate=function(i){c-=i;d-=i;e+=i;g+=i;a()};this.minSelf=function(i){c=c>i.getLeft()?c:i.getLeft();d=d>i.getTop()?d:i.getTop();e=e<i.getRight()?e:i.getRight();g=g<i.getBottom()?g:i.getBottom();a()};this.instersects=function(i){return Math.min(e,i.getRight())-Math.max(c,i.getLeft())>=0&&Math.min(g,i.getBottom())-Math.max(d,i.getTop())>=0};this.empty=function(){b=true;g=e=d=c=0;a()};this.isEmpty=function(){return b};this.toString=
|
||||
function(){return"THREE.Rectangle ( left: "+c+", right: "+e+", top: "+d+", bottom: "+g+", width: "+h+", height: "+o+" )"}};THREE.Matrix3=function(){this.m=[]};THREE.Matrix3.prototype={transpose:function(){var a,c=this.m;a=c[1];c[1]=c[3];c[3]=a;a=c[2];c[2]=c[6];c[6]=a;a=c[5];c[5]=c[7];c[7]=a;return this}};
|
||||
THREE.Matrix4=function(a,c,d,e,g,h,o,b,i,k,y,z,u,x,H,J){this.n11=a||1;this.n12=c||0;this.n13=d||0;this.n14=e||0;this.n21=g||0;this.n22=h||1;this.n23=o||0;this.n24=b||0;this.n31=i||0;this.n32=k||0;this.n33=y||1;this.n34=z||0;this.n41=u||0;this.n42=x||0;this.n43=H||0;this.n44=J||1;this.flat=Array(16);this.m33=new THREE.Matrix3};
|
||||
THREE.Matrix4.prototype={identity:function(){this.n11=1;this.n21=this.n14=this.n13=this.n12=0;this.n22=1;this.n32=this.n31=this.n24=this.n23=0;this.n33=1;this.n43=this.n42=this.n41=this.n34=0;this.n44=1;return this},set:function(a,c,d,e,g,h,o,b,i,k,y,z,u,x,H,J){this.n11=a;this.n12=c;this.n13=d;this.n14=e;this.n21=g;this.n22=h;this.n23=o;this.n24=b;this.n31=i;this.n32=k;this.n33=y;this.n34=z;this.n41=u;this.n42=x;this.n43=H;this.n44=J;return this},copy:function(a){this.n11=a.n11;this.n12=a.n12;this.n13=
|
||||
a.n13;this.n14=a.n14;this.n21=a.n21;this.n22=a.n22;this.n23=a.n23;this.n24=a.n24;this.n31=a.n31;this.n32=a.n32;this.n33=a.n33;this.n34=a.n34;this.n41=a.n41;this.n42=a.n42;this.n43=a.n43;this.n44=a.n44;return this},lookAt:function(a,c,d){var e=THREE.Matrix4.__tmpVec1,g=THREE.Matrix4.__tmpVec2,h=THREE.Matrix4.__tmpVec3;h.sub(a,c).normalize();e.cross(d,h).normalize();g.cross(h,e).normalize();this.n11=e.x;this.n12=e.y;this.n13=e.z;this.n14=-e.dot(a);this.n21=g.x;this.n22=g.y;this.n23=g.z;this.n24=-g.dot(a);
|
||||
this.n31=h.x;this.n32=h.y;this.n33=h.z;this.n34=-h.dot(a);this.n43=this.n42=this.n41=0;this.n44=1;return this},multiplyVector3:function(a){var c=a.x,d=a.y,e=a.z,g=1/(this.n41*c+this.n42*d+this.n43*e+this.n44);a.x=(this.n11*c+this.n12*d+this.n13*e+this.n14)*g;a.y=(this.n21*c+this.n22*d+this.n23*e+this.n24)*g;a.z=(this.n31*c+this.n32*d+this.n33*e+this.n34)*g;return a},multiplyVector4:function(a){var c=a.x,d=a.y,e=a.z,g=a.w;a.x=this.n11*c+this.n12*d+this.n13*e+this.n14*g;a.y=this.n21*c+this.n22*d+this.n23*
|
||||
e+this.n24*g;a.z=this.n31*c+this.n32*d+this.n33*e+this.n34*g;a.w=this.n41*c+this.n42*d+this.n43*e+this.n44*g;return a},crossVector:function(a){var c=new THREE.Vector4;c.x=this.n11*a.x+this.n12*a.y+this.n13*a.z+this.n14*a.w;c.y=this.n21*a.x+this.n22*a.y+this.n23*a.z+this.n24*a.w;c.z=this.n31*a.x+this.n32*a.y+this.n33*a.z+this.n34*a.w;c.w=a.w?this.n41*a.x+this.n42*a.y+this.n43*a.z+this.n44*a.w:1;return c},multiply:function(a,c){var d=a.n11,e=a.n12,g=a.n13,h=a.n14,o=a.n21,b=a.n22,i=a.n23,k=a.n24,y=a.n31,
|
||||
z=a.n32,u=a.n33,x=a.n34,H=a.n41,J=a.n42,K=a.n43,p=a.n44,U=c.n11,F=c.n12,f=c.n13,j=c.n14,q=c.n21,l=c.n22,r=c.n23,C=c.n24,m=c.n31,t=c.n32,v=c.n33,s=c.n34,n=c.n41,E=c.n42,A=c.n43,O=c.n44;this.n11=d*U+e*q+g*m+h*n;this.n12=d*F+e*l+g*t+h*E;this.n13=d*f+e*r+g*v+h*A;this.n14=d*j+e*C+g*s+h*O;this.n21=o*U+b*q+i*m+k*n;this.n22=o*F+b*l+i*t+k*E;this.n23=o*f+b*r+i*v+k*A;this.n24=o*j+b*C+i*s+k*O;this.n31=y*U+z*q+u*m+x*n;this.n32=y*F+z*l+u*t+x*E;this.n33=y*f+z*r+u*v+x*A;this.n34=y*j+z*C+u*s+x*O;this.n41=H*U+J*q+
|
||||
K*m+p*n;this.n42=H*F+J*l+K*t+p*E;this.n43=H*f+J*r+K*v+p*A;this.n44=H*j+J*C+K*s+p*O;return this},multiplySelf:function(a){var c=this.n11,d=this.n12,e=this.n13,g=this.n14,h=this.n21,o=this.n22,b=this.n23,i=this.n24,k=this.n31,y=this.n32,z=this.n33,u=this.n34,x=this.n41,H=this.n42,J=this.n43,K=this.n44,p=a.n11,U=a.n21,F=a.n31,f=a.n41,j=a.n12,q=a.n22,l=a.n32,r=a.n42,C=a.n13,m=a.n23,t=a.n33,v=a.n43,s=a.n14,n=a.n24,E=a.n34;a=a.n44;this.n11=c*p+d*U+e*F+g*f;this.n12=c*j+d*q+e*l+g*r;this.n13=c*C+d*m+e*t+g*
|
||||
v;this.n14=c*s+d*n+e*E+g*a;this.n21=h*p+o*U+b*F+i*f;this.n22=h*j+o*q+b*l+i*r;this.n23=h*C+o*m+b*t+i*v;this.n24=h*s+o*n+b*E+i*a;this.n31=k*p+y*U+z*F+u*f;this.n32=k*j+y*q+z*l+u*r;this.n33=k*C+y*m+z*t+u*v;this.n34=k*s+y*n+z*E+u*a;this.n41=x*p+H*U+J*F+K*f;this.n42=x*j+H*q+J*l+K*r;this.n43=x*C+H*m+J*t+K*v;this.n44=x*s+H*n+J*E+K*a;return this},multiplyScalar:function(a){this.n11*=a;this.n12*=a;this.n13*=a;this.n14*=a;this.n21*=a;this.n22*=a;this.n23*=a;this.n24*=a;this.n31*=a;this.n32*=a;this.n33*=a;this.n34*=
|
||||
a;this.n41*=a;this.n42*=a;this.n43*=a;this.n44*=a;return this},determinant:function(){var a=this.n11,c=this.n12,d=this.n13,e=this.n14,g=this.n21,h=this.n22,o=this.n23,b=this.n24,i=this.n31,k=this.n32,y=this.n33,z=this.n34,u=this.n41,x=this.n42,H=this.n43,J=this.n44;return e*o*k*u-d*b*k*u-e*h*y*u+c*b*y*u+d*h*z*u-c*o*z*u-e*o*i*x+d*b*i*x+e*g*y*x-a*b*y*x-d*g*z*x+a*o*z*x+e*h*i*H-c*b*i*H-e*g*k*H+a*b*k*H+c*g*z*H-a*h*z*H-d*h*i*J+c*o*i*J+d*g*k*J-a*o*k*J-c*g*y*J+a*h*y*J},transpose:function(){function a(c,d,
|
||||
e){var g=c[d];c[d]=c[e];c[e]=g}a(this,"n21","n12");a(this,"n31","n13");a(this,"n32","n23");a(this,"n41","n14");a(this,"n42","n24");a(this,"n43","n34");return this},clone:function(){var a=new THREE.Matrix4;a.n11=this.n11;a.n12=this.n12;a.n13=this.n13;a.n14=this.n14;a.n21=this.n21;a.n22=this.n22;a.n23=this.n23;a.n24=this.n24;a.n31=this.n31;a.n32=this.n32;a.n33=this.n33;a.n34=this.n34;a.n41=this.n41;a.n42=this.n42;a.n43=this.n43;a.n44=this.n44;return a},flatten:function(){var a=this.flat;a[0]=this.n11;
|
||||
a[1]=this.n21;a[2]=this.n31;a[3]=this.n41;a[4]=this.n12;a[5]=this.n22;a[6]=this.n32;a[7]=this.n42;a[8]=this.n13;a[9]=this.n23;a[10]=this.n33;a[11]=this.n43;a[12]=this.n14;a[13]=this.n24;a[14]=this.n34;a[15]=this.n44;return a},setTranslation:function(a,c,d){this.set(1,0,0,a,0,1,0,c,0,0,1,d,0,0,0,1);return this},setScale:function(a,c,d){this.set(a,0,0,0,0,c,0,0,0,0,d,0,0,0,0,1);return this},setRotX:function(a){var c=Math.cos(a);a=Math.sin(a);this.set(1,0,0,0,0,c,-a,0,0,a,c,0,0,0,0,1);return this},setRotY:function(a){var c=
|
||||
Math.cos(a);a=Math.sin(a);this.set(c,0,a,0,0,1,0,0,-a,0,c,0,0,0,0,1);return this},setRotZ:function(a){var c=Math.cos(a);a=Math.sin(a);this.set(c,-a,0,0,a,c,0,0,0,0,1,0,0,0,0,1);return this},setRotAxis:function(a,c){var d=Math.cos(c),e=Math.sin(c),g=1-d,h=a.x,o=a.y,b=a.z,i=g*h,k=g*o;this.set(i*h+d,i*o-e*b,i*b+e*o,0,i*o+e*b,k*o+d,k*b-e*h,0,i*b-e*o,k*b+e*h,g*b*b+d,0,0,0,0,1);return this},toString:function(){return"| "+this.n11+" "+this.n12+" "+this.n13+" "+this.n14+" |\n| "+this.n21+" "+this.n22+" "+
|
||||
this.n23+" "+this.n24+" |\n| "+this.n31+" "+this.n32+" "+this.n33+" "+this.n34+" |\n| "+this.n41+" "+this.n42+" "+this.n43+" "+this.n44+" |"}};THREE.Matrix4.translationMatrix=function(a,c,d){var e=new THREE.Matrix4;e.setTranslation(a,c,d);return e};THREE.Matrix4.scaleMatrix=function(a,c,d){var e=new THREE.Matrix4;e.setScale(a,c,d);return e};THREE.Matrix4.rotationXMatrix=function(a){var c=new THREE.Matrix4;c.setRotX(a);return c};
|
||||
THREE.Matrix4.rotationYMatrix=function(a){var c=new THREE.Matrix4;c.setRotY(a);return c};THREE.Matrix4.rotationZMatrix=function(a){var c=new THREE.Matrix4;c.setRotZ(a);return c};THREE.Matrix4.rotationAxisAngleMatrix=function(a,c){var d=new THREE.Matrix4;d.setRotAxis(a,c);return d};
|
||||
THREE.Matrix4.makeInvert=function(a){var c=a.n11,d=a.n12,e=a.n13,g=a.n14,h=a.n21,o=a.n22,b=a.n23,i=a.n24,k=a.n31,y=a.n32,z=a.n33,u=a.n34,x=a.n41,H=a.n42,J=a.n43,K=a.n44,p=new THREE.Matrix4;p.n11=b*u*H-i*z*H+i*y*J-o*u*J-b*y*K+o*z*K;p.n12=g*z*H-e*u*H-g*y*J+d*u*J+e*y*K-d*z*K;p.n13=e*i*H-g*b*H+g*o*J-d*i*J-e*o*K+d*b*K;p.n14=g*b*y-e*i*y-g*o*z+d*i*z+e*o*u-d*b*u;p.n21=i*z*x-b*u*x-i*k*J+h*u*J+b*k*K-h*z*K;p.n22=e*u*x-g*z*x+g*k*J-c*u*J-e*k*K+c*z*K;p.n23=g*b*x-e*i*x-g*h*J+c*i*J+e*h*K-c*b*K;p.n24=e*i*k-g*b*k+
|
||||
g*h*z-c*i*z-e*h*u+c*b*u;p.n31=o*u*x-i*y*x+i*k*H-h*u*H-o*k*K+h*y*K;p.n32=g*y*x-d*u*x-g*k*H+c*u*H+d*k*K-c*y*K;p.n33=e*i*x-g*o*x+g*h*H-c*i*H-d*h*K+c*o*K;p.n34=g*o*k-d*i*k-g*h*y+c*i*y+d*h*u-c*o*u;p.n41=b*y*x-o*z*x-b*k*H+h*z*H+o*k*J-h*y*J;p.n42=d*z*x-e*y*x+e*k*H-c*z*H-d*k*J+c*y*J;p.n43=e*o*x-d*b*x-e*h*H+c*b*H+d*h*J-c*o*J;p.n44=d*b*k-e*o*k+e*h*y-c*b*y-d*h*z+c*o*z;p.multiplyScalar(1/a.determinant());return p};
|
||||
THREE.Matrix4.makeInvert3x3=function(a){var c=a.flatten();a=a.m33;var d=a.m,e=c[10]*c[5]-c[6]*c[9],g=-c[10]*c[1]+c[2]*c[9],h=c[6]*c[1]-c[2]*c[5],o=-c[10]*c[4]+c[6]*c[8],b=c[10]*c[0]-c[2]*c[8],i=-c[6]*c[0]+c[2]*c[4],k=c[9]*c[4]-c[5]*c[8],y=-c[9]*c[0]+c[1]*c[8],z=c[5]*c[0]-c[1]*c[4];c=c[0]*e+c[1]*o+c[2]*k;if(c==0)throw"matrix not invertible";c=1/c;d[0]=c*e;d[1]=c*g;d[2]=c*h;d[3]=c*o;d[4]=c*b;d[5]=c*i;d[6]=c*k;d[7]=c*y;d[8]=c*z;return a};
|
||||
THREE.Matrix4.makeFrustum=function(a,c,d,e,g,h){var o,b,i;o=new THREE.Matrix4;b=2*g/(c-a);i=2*g/(e-d);a=(c+a)/(c-a);d=(e+d)/(e-d);e=-(h+g)/(h-g);g=-2*h*g/(h-g);o.n11=b;o.n12=0;o.n13=a;o.n14=0;o.n21=0;o.n22=i;o.n23=d;o.n24=0;o.n31=0;o.n32=0;o.n33=e;o.n34=g;o.n41=0;o.n42=0;o.n43=-1;o.n44=0;return o};THREE.Matrix4.makePerspective=function(a,c,d,e){var g;a=d*Math.tan(a*Math.PI/360);g=-a;return THREE.Matrix4.makeFrustum(g*c,a*c,g,a,d,e)};
|
||||
THREE.Matrix4.makeOrtho=function(a,c,d,e,g,h){var o,b,i,k;o=new THREE.Matrix4;b=c-a;i=d-e;k=h-g;a=(c+a)/b;d=(d+e)/i;g=(h+g)/k;o.n11=2/b;o.n12=0;o.n13=0;o.n14=-a;o.n21=0;o.n22=2/i;o.n23=0;o.n24=-d;o.n31=0;o.n32=0;o.n33=-2/k;o.n34=-g;o.n41=0;o.n42=0;o.n43=0;o.n44=1;return o};THREE.Matrix4.__tmpVec1=new THREE.Vector3;THREE.Matrix4.__tmpVec2=new THREE.Vector3;THREE.Matrix4.__tmpVec3=new THREE.Vector3;
|
||||
THREE.Vertex=function(a,c){this.position=a||new THREE.Vector3;this.positionWorld=new THREE.Vector3;this.positionScreen=new THREE.Vector4;this.normal=c||new THREE.Vector3;this.normalWorld=new THREE.Vector3;this.normalScreen=new THREE.Vector3;this.tangent=new THREE.Vector4;this.__visible=true};THREE.Vertex.prototype={toString:function(){return"THREE.Vertex ( position: "+this.position+", normal: "+this.normal+" )"}};
|
||||
THREE.Face3=function(a,c,d,e,g){this.a=a;this.b=c;this.c=d;this.centroid=new THREE.Vector3;this.normal=e instanceof THREE.Vector3?e:new THREE.Vector3;this.vertexNormals=e instanceof Array?e:[];this.materials=g instanceof Array?g:[g]};THREE.Face3.prototype={toString:function(){return"THREE.Face3 ( "+this.a+", "+this.b+", "+this.c+" )"}};
|
||||
THREE.Face4=function(a,c,d,e,g,h){this.a=a;this.b=c;this.c=d;this.d=e;this.centroid=new THREE.Vector3;this.normal=g instanceof THREE.Vector3?g:new THREE.Vector3;this.vertexNormals=g instanceof Array?g:[];this.materials=h instanceof Array?h:[h]};THREE.Face4.prototype={toString:function(){return"THREE.Face4 ( "+this.a+", "+this.b+", "+this.c+" "+this.d+" )"}};THREE.UV=function(a,c){this.u=a||0;this.v=c||0};
|
||||
THREE.UV.prototype={copy:function(a){this.u=a.u;this.v=a.v},toString:function(){return"THREE.UV ("+this.u+", "+this.v+")"}};THREE.Geometry=function(){this.vertices=[];this.faces=[];this.uvs=[];this.boundingSphere=this.boundingBox=null;this.geometryChunks={};this.hasTangents=false};
|
||||
THREE.Geometry.prototype={computeCentroids:function(){var a,c,d;a=0;for(c=this.faces.length;a<c;a++){d=this.faces[a];d.centroid.set(0,0,0);if(d instanceof THREE.Face3){d.centroid.addSelf(this.vertices[d.a].position);d.centroid.addSelf(this.vertices[d.b].position);d.centroid.addSelf(this.vertices[d.c].position);d.centroid.divideScalar(3)}else if(d instanceof THREE.Face4){d.centroid.addSelf(this.vertices[d.a].position);d.centroid.addSelf(this.vertices[d.b].position);d.centroid.addSelf(this.vertices[d.c].position);
|
||||
d.centroid.addSelf(this.vertices[d.d].position);d.centroid.divideScalar(4)}}},computeFaceNormals:function(a){var c,d,e,g,h,o,b=new THREE.Vector3,i=new THREE.Vector3;e=0;for(g=this.vertices.length;e<g;e++){h=this.vertices[e];h.normal.set(0,0,0)}e=0;for(g=this.faces.length;e<g;e++){h=this.faces[e];if(a&&h.vertexNormals.length){b.set(0,0,0);c=0;for(d=h.normal.length;c<d;c++)b.addSelf(h.vertexNormals[c]);b.divideScalar(3)}else{c=this.vertices[h.a];d=this.vertices[h.b];o=this.vertices[h.c];b.sub(o.position,
|
||||
d.position);i.sub(c.position,d.position);b.crossSelf(i)}b.isZero()||b.normalize();h.normal.copy(b)}},computeVertexNormals:function(){var a,c,d,e;if(this.__tmpVertices==undefined){e=this.__tmpVertices=Array(this.vertices.length);a=0;for(c=this.vertices.length;a<c;a++)e[a]=new THREE.Vector3;a=0;for(c=this.faces.length;a<c;a++){d=this.faces[a];if(d instanceof THREE.Face3)d.vertexNormals=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];else if(d instanceof THREE.Face4)d.vertexNormals=[new THREE.Vector3,
|
||||
new THREE.Vector3,new THREE.Vector3,new THREE.Vector3]}}else{e=this.__tmpVertices;a=0;for(c=this.vertices.length;a<c;a++)e[a].set(0,0,0)}a=0;for(c=this.faces.length;a<c;a++){d=this.faces[a];if(d instanceof THREE.Face3){e[d.a].addSelf(d.normal);e[d.b].addSelf(d.normal);e[d.c].addSelf(d.normal)}else if(d instanceof THREE.Face4){e[d.a].addSelf(d.normal);e[d.b].addSelf(d.normal);e[d.c].addSelf(d.normal);e[d.d].addSelf(d.normal)}}a=0;for(c=this.vertices.length;a<c;a++)e[a].normalize();a=0;for(c=this.faces.length;a<
|
||||
c;a++){d=this.faces[a];if(d instanceof THREE.Face3){d.vertexNormals[0].copy(e[d.a]);d.vertexNormals[1].copy(e[d.b]);d.vertexNormals[2].copy(e[d.c])}else if(d instanceof THREE.Face4){d.vertexNormals[0].copy(e[d.a]);d.vertexNormals[1].copy(e[d.b]);d.vertexNormals[2].copy(e[d.c]);d.vertexNormals[3].copy(e[d.d])}}},computeTangents:function(){function a(s,n,E,A,O,N,G){h=s.vertices[n].position;o=s.vertices[E].position;b=s.vertices[A].position;i=g[O];k=g[N];y=g[G];z=o.x-h.x;u=b.x-h.x;x=o.y-h.y;H=b.y-h.y;
|
||||
J=o.z-h.z;K=b.z-h.z;p=k.u-i.u;U=y.u-i.u;F=k.v-i.v;f=y.v-i.v;j=1/(p*f-U*F);r.set((f*z-F*u)*j,(f*x-F*H)*j,(f*J-F*K)*j);C.set((p*u-U*z)*j,(p*H-U*x)*j,(p*K-U*J)*j);q[n].addSelf(r);q[E].addSelf(r);q[A].addSelf(r);l[n].addSelf(C);l[E].addSelf(C);l[A].addSelf(C)}var c,d,e,g,h,o,b,i,k,y,z,u,x,H,J,K,p,U,F,f,j,q=[],l=[],r=new THREE.Vector3,C=new THREE.Vector3,m=new THREE.Vector3,t=new THREE.Vector3,v=new THREE.Vector3;c=0;for(d=this.vertices.length;c<d;c++){q[c]=new THREE.Vector3;l[c]=new THREE.Vector3}c=0;
|
||||
for(d=this.faces.length;c<d;c++){e=this.faces[c];g=this.uvs[c];if(e instanceof THREE.Face3){a(this,e.a,e.b,e.c,0,1,2);this.vertices[e.a].normal.copy(e.vertexNormals[0]);this.vertices[e.b].normal.copy(e.vertexNormals[1]);this.vertices[e.c].normal.copy(e.vertexNormals[2])}else if(e instanceof THREE.Face4){a(this,e.a,e.b,e.c,0,1,2);a(this,e.a,e.b,e.d,0,1,3);this.vertices[e.a].normal.copy(e.vertexNormals[0]);this.vertices[e.b].normal.copy(e.vertexNormals[1]);this.vertices[e.c].normal.copy(e.vertexNormals[2]);
|
||||
this.vertices[e.d].normal.copy(e.vertexNormals[3])}}c=0;for(d=this.vertices.length;c<d;c++){v.copy(this.vertices[c].normal);e=q[c];m.copy(e);m.subSelf(v.multiplyScalar(v.dot(e))).normalize();t.cross(this.vertices[c].normal,e);e=t.dot(l[c]);e=e<0?-1:1;this.vertices[c].tangent.set(m.x,m.y,m.z,e)}this.hasTangents=true},computeBoundingBox:function(){var a;if(this.vertices.length>0){this.boundingBox={x:[this.vertices[0].position.x,this.vertices[0].position.x],y:[this.vertices[0].position.y,this.vertices[0].position.y],
|
||||
z:[this.vertices[0].position.z,this.vertices[0].position.z]};for(var c=1,d=this.vertices.length;c<d;c++){a=this.vertices[c];if(a.position.x<this.boundingBox.x[0])this.boundingBox.x[0]=a.position.x;else if(a.position.x>this.boundingBox.x[1])this.boundingBox.x[1]=a.position.x;if(a.position.y<this.boundingBox.y[0])this.boundingBox.y[0]=a.position.y;else if(a.position.y>this.boundingBox.y[1])this.boundingBox.y[1]=a.position.y;if(a.position.z<this.boundingBox.z[0])this.boundingBox.z[0]=a.position.z;else if(a.position.z>
|
||||
this.boundingBox.z[1])this.boundingBox.z[1]=a.position.z}}},computeBoundingSphere:function(){for(var a=this.boundingSphere===null?0:this.boundingSphere.radius,c=0,d=this.vertices.length;c<d;c++)a=Math.max(a,this.vertices[c].position.length());this.boundingSphere={radius:a}},sortFacesByMaterial:function(){function a(y){var z=[];c=0;for(d=y.length;c<d;c++)y[c]==undefined?z.push("undefined"):z.push(y[c].toString());return z.join("_")}var c,d,e,g,h,o,b,i,k={};e=0;for(g=this.faces.length;e<g;e++){h=this.faces[e];
|
||||
o=h.materials;b=a(o);if(k[b]==undefined)k[b]={hash:b,counter:0};i=k[b].hash+"_"+k[b].counter;if(this.geometryChunks[i]==undefined)this.geometryChunks[i]={faces:[],materials:o,vertices:0};h=h instanceof THREE.Face3?3:4;if(this.geometryChunks[i].vertices+h>65535){k[b].counter+=1;i=k[b].hash+"_"+k[b].counter;if(this.geometryChunks[i]==undefined)this.geometryChunks[i]={faces:[],materials:o,vertices:0}}this.geometryChunks[i].faces.push(e);this.geometryChunks[i].vertices+=h}},toString:function(){return"THREE.Geometry ( vertices: "+
|
||||
this.vertices+", faces: "+this.faces+", uvs: "+this.uvs+" )"}};
|
||||
THREE.Camera=function(a,c,d,e){this.fov=a;this.aspect=c;this.near=d;this.far=e;this.position=new THREE.Vector3;this.target={position:new THREE.Vector3};this.autoUpdateMatrix=true;this.projectionMatrix=null;this.matrix=new THREE.Matrix4;this.up=new THREE.Vector3(0,1,0);this.tmpVec=new THREE.Vector3;this.translateX=function(g){this.tmpVec.sub(this.target.position,this.position).normalize().multiplyScalar(g);this.tmpVec.crossSelf(this.up);this.position.addSelf(this.tmpVec);this.target.position.addSelf(this.tmpVec)};
|
||||
this.translateZ=function(g){this.tmpVec.sub(this.target.position,this.position).normalize().multiplyScalar(g);this.position.subSelf(this.tmpVec);this.target.position.subSelf(this.tmpVec)};this.updateMatrix=function(){this.matrix.lookAt(this.position,this.target.position,this.up)};this.updateProjectionMatrix=function(){this.projectionMatrix=THREE.Matrix4.makePerspective(this.fov,this.aspect,this.near,this.far)};this.updateProjectionMatrix()};
|
||||
THREE.Camera.prototype={toString:function(){return"THREE.Camera ( "+this.position+", "+this.target.position+" )"}};THREE.Light=function(a){this.color=new THREE.Color(a)};THREE.AmbientLight=function(a){THREE.Light.call(this,a)};THREE.AmbientLight.prototype=new THREE.Light;THREE.AmbientLight.prototype.constructor=THREE.AmbientLight;THREE.DirectionalLight=function(a,c){THREE.Light.call(this,a);this.position=new THREE.Vector3(0,1,0);this.intensity=c||1};THREE.DirectionalLight.prototype=new THREE.Light;
|
||||
THREE.DirectionalLight.prototype.constructor=THREE.DirectionalLight;THREE.PointLight=function(a,c){THREE.Light.call(this,a);this.position=new THREE.Vector3;this.intensity=c||1};THREE.PointLight.prototype=new THREE.Light;THREE.PointLight.prototype.constructor=THREE.PointLight;
|
||||
THREE.Object3D=function(){this.id=THREE.Object3DCounter.value++;this.position=new THREE.Vector3;this.rotation=new THREE.Vector3;this.scale=new THREE.Vector3(1,1,1);this.matrix=new THREE.Matrix4;this.rotationMatrix=new THREE.Matrix4;this.tmpMatrix=new THREE.Matrix4;this.screen=new THREE.Vector3;this.visible=this.autoUpdateMatrix=true};
|
||||
THREE.Object3D.prototype={updateMatrix:function(){var a=this.position,c=this.rotation,d=this.scale,e=this.tmpMatrix;this.matrix.setTranslation(a.x,a.y,a.z);this.rotationMatrix.setRotX(c.x);if(c.y!=0){e.setRotY(c.y);this.rotationMatrix.multiplySelf(e)}if(c.z!=0){e.setRotZ(c.z);this.rotationMatrix.multiplySelf(e)}this.matrix.multiplySelf(this.rotationMatrix);if(d.x!=0||d.y!=0||d.z!=0){e.setScale(d.x,d.y,d.z);this.matrix.multiplySelf(e)}}};THREE.Object3DCounter={value:0};
|
||||
THREE.Particle=function(a){THREE.Object3D.call(this);this.materials=a instanceof Array?a:[a];this.autoUpdateMatrix=false};THREE.Particle.prototype=new THREE.Object3D;THREE.Particle.prototype.constructor=THREE.Particle;THREE.ParticleSystem=function(a,c){THREE.Object3D.call(this);this.geometry=a;this.materials=c instanceof Array?c:[c];this.autoUpdateMatrix=false};THREE.ParticleSystem.prototype=new THREE.Object3D;THREE.ParticleSystem.prototype.constructor=THREE.ParticleSystem;
|
||||
THREE.Line=function(a,c,d){THREE.Object3D.call(this);this.geometry=a;this.materials=c instanceof Array?c:[c];this.type=d!=undefined?d:THREE.LineStrip};THREE.LineStrip=0;THREE.LinePieces=1;THREE.Line.prototype=new THREE.Object3D;THREE.Line.prototype.constructor=THREE.Line;THREE.Mesh=function(a,c){THREE.Object3D.call(this);this.geometry=a;this.materials=c instanceof Array?c:[c];this.overdraw=this.doubleSided=this.flipSided=false;this.geometry.boundingSphere||this.geometry.computeBoundingSphere()};
|
||||
THREE.Mesh.prototype=new THREE.Object3D;THREE.Mesh.prototype.constructor=THREE.Mesh;THREE.FlatShading=0;THREE.SmoothShading=1;THREE.NormalBlending=0;THREE.AdditiveBlending=1;THREE.SubtractiveBlending=2;
|
||||
THREE.LineBasicMaterial=function(a){this.color=new THREE.Color(16777215);this.opacity=1;this.blending=THREE.NormalBlending;this.linewidth=1;this.linejoin=this.linecap="round";if(a){a.color!==undefined&&this.color.setHex(a.color);if(a.opacity!==undefined)this.opacity=a.opacity;if(a.blending!==undefined)this.blending=a.blending;if(a.linewidth!==undefined)this.linewidth=a.linewidth;if(a.linecap!==undefined)this.linecap=a.linecap;if(a.linejoin!==undefined)this.linejoin=a.linejoin}};
|
||||
THREE.LineBasicMaterial.prototype={toString:function(){return"THREE.LineBasicMaterial (<br/>color: "+this.color+"<br/>opacity: "+this.opacity+"<br/>blending: "+this.blending+"<br/>linewidth: "+this.linewidth+"<br/>linecap: "+this.linecap+"<br/>linejoin: "+this.linejoin+"<br/>)"}};
|
||||
THREE.MeshBasicMaterial=function(a){this.id=THREE.MeshBasicMaterialCounter.value++;this.color=new THREE.Color(16777215);this.env_map=this.map=null;this.combine=THREE.MultiplyOperation;this.reflectivity=1;this.refraction_ratio=0.98;this.fog=true;this.opacity=1;this.shading=THREE.SmoothShading;this.blending=THREE.NormalBlending;this.wireframe=false;this.wireframe_linewidth=1;this.wireframe_linejoin=this.wireframe_linecap="round";if(a){a.color!==undefined&&this.color.setHex(a.color);if(a.map!==undefined)this.map=
|
||||
a.map;if(a.env_map!==undefined)this.env_map=a.env_map;if(a.combine!==undefined)this.combine=a.combine;if(a.reflectivity!==undefined)this.reflectivity=a.reflectivity;if(a.refraction_ratio!==undefined)this.refraction_ratio=a.refraction_ratio;if(a.fog!==undefined)this.fog=a.fog;if(a.opacity!==undefined)this.opacity=a.opacity;if(a.shading!==undefined)this.shading=a.shading;if(a.blending!==undefined)this.blending=a.blending;if(a.wireframe!==undefined)this.wireframe=a.wireframe;if(a.wireframe_linewidth!==
|
||||
undefined)this.wireframe_linewidth=a.wireframe_linewidth;if(a.wireframe_linecap!==undefined)this.wireframe_linecap=a.wireframe_linecap;if(a.wireframe_linejoin!==undefined)this.wireframe_linejoin=a.wireframe_linejoin}};
|
||||
THREE.MeshBasicMaterial.prototype={toString:function(){return"THREE.MeshBasicMaterial (<br/>id: "+this.id+"<br/>color: "+this.color+"<br/>map: "+this.map+"<br/>env_map: "+this.env_map+"<br/>combine: "+this.combine+"<br/>reflectivity: "+this.reflectivity+"<br/>refraction_ratio: "+this.refraction_ratio+"<br/>opacity: "+this.opacity+"<br/>blending: "+this.blending+"<br/>wireframe: "+this.wireframe+"<br/>wireframe_linewidth: "+this.wireframe_linewidth+"<br/>wireframe_linecap: "+this.wireframe_linecap+
|
||||
"<br/>wireframe_linejoin: "+this.wireframe_linejoin+"<br/>)"}};THREE.MeshBasicMaterialCounter={value:0};
|
||||
THREE.MeshLambertMaterial=function(a){this.id=THREE.MeshLambertMaterialCounter.value++;this.color=new THREE.Color(16777215);this.env_map=this.map=null;this.combine=THREE.MultiplyOperation;this.reflectivity=1;this.refraction_ratio=0.98;this.fog=true;this.opacity=1;this.shading=THREE.SmoothShading;this.blending=THREE.NormalBlending;this.wireframe=false;this.wireframe_linewidth=1;this.wireframe_linejoin=this.wireframe_linecap="round";if(a){a.color!==undefined&&this.color.setHex(a.color);if(a.map!==undefined)this.map=
|
||||
a.map;if(a.env_map!==undefined)this.env_map=a.env_map;if(a.combine!==undefined)this.combine=a.combine;if(a.reflectivity!==undefined)this.reflectivity=a.reflectivity;if(a.refraction_ratio!==undefined)this.refraction_ratio=a.refraction_ratio;if(a.fog!==undefined)this.fog=a.fog;if(a.opacity!==undefined)this.opacity=a.opacity;if(a.shading!==undefined)this.shading=a.shading;if(a.blending!==undefined)this.blending=a.blending;if(a.wireframe!==undefined)this.wireframe=a.wireframe;if(a.wireframe_linewidth!==
|
||||
undefined)this.wireframe_linewidth=a.wireframe_linewidth;if(a.wireframe_linecap!==undefined)this.wireframe_linecap=a.wireframe_linecap;if(a.wireframe_linejoin!==undefined)this.wireframe_linejoin=a.wireframe_linejoin}};
|
||||
THREE.MeshLambertMaterial.prototype={toString:function(){return"THREE.MeshLambertMaterial (<br/>id: "+this.id+"<br/>color: "+this.color+"<br/>map: "+this.map+"<br/>env_map: "+this.env_map+"<br/>combine: "+this.combine+"<br/>reflectivity: "+this.reflectivity+"<br/>refraction_ratio: "+this.refraction_ratio+"<br/>opacity: "+this.opacity+"<br/>shading: "+this.shading+"<br/>blending: "+this.blending+"<br/>wireframe: "+this.wireframe+"<br/>wireframe_linewidth: "+this.wireframe_linewidth+"<br/>wireframe_linecap: "+
|
||||
this.wireframe_linecap+"<br/>wireframe_linejoin: "+this.wireframe_linejoin+"<br/> )"}};THREE.MeshLambertMaterialCounter={value:0};
|
||||
THREE.MeshPhongMaterial=function(a){this.id=THREE.MeshPhongMaterialCounter.value++;this.color=new THREE.Color(16777215);this.ambient=new THREE.Color(328965);this.specular=new THREE.Color(1118481);this.shininess=30;this.env_map=this.specular_map=this.map=null;this.combine=THREE.MultiplyOperation;this.reflectivity=1;this.refraction_ratio=0.98;this.fog=true;this.opacity=1;this.shading=THREE.SmoothShading;this.blending=THREE.NormalBlending;this.wireframe=false;this.wireframe_linewidth=1;this.wireframe_linejoin=
|
||||
this.wireframe_linecap="round";if(a){if(a.color!==undefined)this.color=new THREE.Color(a.color);if(a.ambient!==undefined)this.ambient=new THREE.Color(a.ambient);if(a.specular!==undefined)this.specular=new THREE.Color(a.specular);if(a.shininess!==undefined)this.shininess=a.shininess;if(a.map!==undefined)this.map=a.map;if(a.specular_map!==undefined)this.specular_map=a.specular_map;if(a.env_map!==undefined)this.env_map=a.env_map;if(a.combine!==undefined)this.combine=a.combine;if(a.reflectivity!==undefined)this.reflectivity=
|
||||
a.reflectivity;if(a.refraction_ratio!==undefined)this.refraction_ratio=a.refraction_ratio;if(a.fog!==undefined)this.fog=a.fog;if(a.opacity!==undefined)this.opacity=a.opacity;if(a.shading!==undefined)this.shading=a.shading;if(a.blending!==undefined)this.blending=a.blending;if(a.wireframe!==undefined)this.wireframe=a.wireframe;if(a.wireframe_linewidth!==undefined)this.wireframe_linewidth=a.wireframe_linewidth;if(a.wireframe_linecap!==undefined)this.wireframe_linecap=a.wireframe_linecap;if(a.wireframe_linejoin!==
|
||||
undefined)this.wireframe_linejoin=a.wireframe_linejoin}};
|
||||
THREE.MeshPhongMaterial.prototype={toString:function(){return"THREE.MeshPhongMaterial (<br/>id: "+this.id+"<br/>color: "+this.color+"<br/>ambient: "+this.ambient+"<br/>specular: "+this.specular+"<br/>shininess: "+this.shininess+"<br/>map: "+this.map+"<br/>specular_map: "+this.specular_map+"<br/>env_map: "+this.env_map+"<br/>combine: "+this.combine+"<br/>reflectivity: "+this.reflectivity+"<br/>refraction_ratio: "+this.refraction_ratio+"<br/>opacity: "+this.opacity+"<br/>shading: "+this.shading+"<br/>wireframe: "+
|
||||
this.wireframe+"<br/>wireframe_linewidth: "+this.wireframe_linewidth+"<br/>wireframe_linecap: "+this.wireframe_linecap+"<br/>wireframe_linejoin: "+this.wireframe_linejoin+"<br/>)"}};THREE.MeshPhongMaterialCounter={value:0};
|
||||
THREE.MeshDepthMaterial=function(a){this.opacity=1;this.shading=THREE.SmoothShading;this.blending=THREE.NormalBlending;this.wireframe=false;this.wireframe_linewidth=1;this.wireframe_linejoin=this.wireframe_linecap="round";if(a){if(a.opacity!==undefined)this.opacity=a.opacity;if(a.blending!==undefined)this.blending=a.blending}};THREE.MeshDepthMaterial.prototype={toString:function(){return"THREE.MeshDepthMaterial"}};
|
||||
THREE.MeshNormalMaterial=function(a){this.opacity=1;this.shading=THREE.FlatShading;this.blending=THREE.NormalBlending;if(a){if(a.opacity!==undefined)this.opacity=a.opacity;if(a.shading!==undefined)this.shading=a.shading;if(a.blending!==undefined)this.blending=a.blending}};THREE.MeshNormalMaterial.prototype={toString:function(){return"THREE.MeshNormalMaterial"}};THREE.MeshFaceMaterial=function(){};THREE.MeshFaceMaterial.prototype={toString:function(){return"THREE.MeshFaceMaterial"}};
|
||||
THREE.MeshShaderMaterial=function(a){this.id=THREE.MeshShaderMaterialCounter.value++;this.vertex_shader=this.fragment_shader="void main() {}";this.uniforms={};this.opacity=1;this.shading=THREE.SmoothShading;this.blending=THREE.NormalBlending;this.wireframe=false;this.wireframe_linewidth=1;this.wireframe_linejoin=this.wireframe_linecap="round";if(a){if(a.fragment_shader!==undefined)this.fragment_shader=a.fragment_shader;if(a.vertex_shader!==undefined)this.vertex_shader=a.vertex_shader;if(a.uniforms!==
|
||||
undefined)this.uniforms=a.uniforms;if(a.shading!==undefined)this.shading=a.shading;if(a.blending!==undefined)this.blending=a.blending;if(a.wireframe!==undefined)this.wireframe=a.wireframe;if(a.wireframe_linewidth!==undefined)this.wireframe_linewidth=a.wireframe_linewidth;if(a.wireframe_linecap!==undefined)this.wireframe_linecap=a.wireframe_linecap;if(a.wireframe_linejoin!==undefined)this.wireframe_linejoin=a.wireframe_linejoin}};
|
||||
THREE.MeshShaderMaterial.prototype={toString:function(){return"THREE.MeshShaderMaterial (<br/>id: "+this.id+"<br/>blending: "+this.blending+"<br/>wireframe: "+this.wireframe+"<br/>wireframe_linewidth: "+this.wireframe_linewidth+"<br/>wireframe_linecap: "+this.wireframe_linecap+"<br/>wireframe_linejoin: "+this.wireframe_linejoin+"<br/>)"}};THREE.MeshShaderMaterialCounter={value:0};
|
||||
THREE.ParticleBasicMaterial=function(a){this.color=new THREE.Color(16777215);this.map=null;this.opacity=1;this.blending=THREE.NormalBlending;this.offset=new THREE.Vector2;if(a){a.color!==undefined&&this.color.setHex(a.color);if(a.map!==undefined)this.map=a.map;if(a.opacity!==undefined)this.opacity=a.opacity;if(a.blending!==undefined)this.blending=a.blending}};
|
||||
THREE.ParticleBasicMaterial.prototype={toString:function(){return"THREE.ParticleBasicMaterial (<br/>color: "+this.color+"<br/>map: "+this.map+"<br/>opacity: "+this.opacity+"<br/>blending: "+this.blending+"<br/>)"}};THREE.ParticleCircleMaterial=function(a){this.color=new THREE.Color(16777215);this.opacity=1;this.blending=THREE.NormalBlending;if(a){a.color!==undefined&&this.color.setHex(a.color);if(a.opacity!==undefined)this.opacity=a.opacity;if(a.blending!==undefined)this.blending=a.blending}};
|
||||
THREE.ParticleCircleMaterial.prototype={toString:function(){return"THREE.ParticleCircleMaterial (<br/>color: "+this.color+"<br/>opacity: "+this.opacity+"<br/>blending: "+this.blending+"<br/>)"}};THREE.ParticleDOMMaterial=function(a){this.domElement=a};THREE.ParticleDOMMaterial.prototype={toString:function(){return"THREE.ParticleDOMMaterial ( domElement: "+this.domElement+" )"}};
|
||||
THREE.Texture=function(a,c,d,e,g,h){this.image=a;this.mapping=c!==undefined?c:new THREE.UVMapping;this.wrap_s=d!==undefined?d:THREE.ClampToEdgeWrapping;this.wrap_t=e!==undefined?e:THREE.ClampToEdgeWrapping;this.mag_filter=g!==undefined?g:THREE.LinearFilter;this.min_filter=h!==undefined?h:THREE.LinearMipMapLinearFilter};
|
||||
THREE.Texture.prototype={clone:function(){return new THREE.Texture(this.image,this.mapping,this.wrap_s,this.wrap_t,this.mag_filter,this.min_filter)},toString:function(){return"THREE.Texture (<br/>image: "+this.image+"<br/>wrap_s: "+this.wrap_s+"<br/>wrap_t: "+this.wrap_t+"<br/>mag_filter: "+this.mag_filter+"<br/>min_filter: "+this.min_filter+"<br/>)"}};THREE.MultiplyOperation=0;THREE.MixOperation=1;THREE.RepeatWrapping=0;THREE.ClampToEdgeWrapping=1;THREE.MirroredRepeatWrapping=2;
|
||||
THREE.NearestFilter=3;THREE.NearestMipMapNearestFilter=4;THREE.NearestMipMapLinearFilter=5;THREE.LinearFilter=6;THREE.LinearMipMapNearestFilter=7;THREE.LinearMipMapLinearFilter=8;THREE.ByteType=9;THREE.UnsignedByteType=10;THREE.ShortType=11;THREE.UnsignedShortType=12;THREE.IntType=13;THREE.UnsignedIntType=14;THREE.FloatType=15;THREE.AlphaFormat=16;THREE.RGBFormat=17;THREE.RGBAFormat=18;THREE.LuminanceFormat=19;THREE.LuminanceAlphaFormat=20;
|
||||
THREE.RenderTarget=function(a,c,d){this.width=a;this.height=c;d=d||{};this.wrap_s=d.wrap_s!==undefined?d.wrap_s:THREE.ClampToEdgeWrapping;this.wrap_t=d.wrap_t!==undefined?d.wrap_t:THREE.ClampToEdgeWrapping;this.mag_filter=d.mag_filter!==undefined?d.mag_filter:THREE.LinearFilter;this.min_filter=d.min_filter!==undefined?d.min_filter:THREE.LinearMipMapLinearFilter;this.format=d.format!==undefined?d.format:THREE.RGBFormat;this.type=d.type!==undefined?d.type:THREE.UnsignedByteType};
|
||||
var Uniforms={clone:function(a){var c,d,e,g={};for(c in a){g[c]={};for(d in a[c]){e=a[c][d];g[c][d]=e instanceof THREE.Color||e instanceof THREE.Vector3||e instanceof THREE.Texture?e.clone():e}}return g},merge:function(a){var c,d,e,g={};for(c=0;c<a.length;c++){e=this.clone(a[c]);for(d in e)g[d]=e[d]}return g}};THREE.CubeReflectionMapping=function(){};THREE.CubeRefractionMapping=function(){};THREE.LatitudeReflectionMapping=function(){};THREE.LatitudeRefractionMapping=function(){};
|
||||
THREE.SphericalReflectionMapping=function(){};THREE.SphericalRefractionMapping=function(){};THREE.UVMapping=function(){};
|
||||
THREE.Scene=function(){this.objects=[];this.lights=[];this.fog=null;this.addObject=function(a){this.objects.indexOf(a)===-1&&this.objects.push(a)};this.removeObject=function(a){a=this.objects.indexOf(a);a!==-1&&this.objects.splice(a,1)};this.addLight=function(a){this.lights.indexOf(a)===-1&&this.lights.push(a)};this.removeLight=function(a){a=this.lights.indexOf(a);a!==-1&&this.lights.splice(a,1)};this.toString=function(){return"THREE.Scene ( "+this.objects+" )"}};
|
||||
THREE.Fog=function(a,c,d){this.color=new THREE.Color(a);this.near=c||1;this.far=d||1E3};THREE.FogExp2=function(a,c){this.color=new THREE.Color(a);this.density=c||2.5E-4};
|
||||
THREE.Projector=function(){function a(l,r){return r.z-l.z}function c(l,r){var C=0,m=1,t=l.z+l.w,v=r.z+r.w,s=-l.z+l.w,n=-r.z+r.w;if(t>=0&&v>=0&&s>=0&&n>=0)return true;else if(t<0&&v<0||s<0&&n<0)return false;else{if(t<0)C=Math.max(C,t/(t-v));else if(v<0)m=Math.min(m,t/(t-v));if(s<0)C=Math.max(C,s/(s-n));else if(n<0)m=Math.min(m,s/(s-n));if(m<C)return false;else{l.lerpSelf(r,C);r.lerpSelf(l,1-m);return true}}}var d,e,g=[],h,o,b,i=[],k,y,z=[],u,x,H=[],J=new THREE.Vector4,K=new THREE.Vector4,p=new THREE.Matrix4,
|
||||
U=new THREE.Matrix4,F=[],f=new THREE.Vector4,j=new THREE.Vector4,q;this.projectObjects=function(l,r,C){var m=[],t,v;e=0;p.multiply(r.projectionMatrix,r.matrix);F[0]=new THREE.Vector4(p.n41-p.n11,p.n42-p.n12,p.n43-p.n13,p.n44-p.n14);F[1]=new THREE.Vector4(p.n41+p.n11,p.n42+p.n12,p.n43+p.n13,p.n44+p.n14);F[2]=new THREE.Vector4(p.n41+p.n21,p.n42+p.n22,p.n43+p.n23,p.n44+p.n24);F[3]=new THREE.Vector4(p.n41-p.n21,p.n42-p.n22,p.n43-p.n23,p.n44-p.n24);F[4]=new THREE.Vector4(p.n41-p.n31,p.n42-p.n32,p.n43-
|
||||
p.n33,p.n44-p.n34);F[5]=new THREE.Vector4(p.n41+p.n31,p.n42+p.n32,p.n43+p.n33,p.n44+p.n34);r=0;for(t=F.length;r<t;r++){v=F[r];v.divideScalar(Math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z))}t=l.objects;l=0;for(r=t.length;l<r;l++){v=t[l];var s;if(!(s=!v.visible)){if(s=v instanceof THREE.Mesh){a:{s=void 0;for(var n=v.position,E=-v.geometry.boundingSphere.radius*Math.max(v.scale.x,Math.max(v.scale.y,v.scale.z)),A=0;A<6;A++){s=F[A].x*n.x+F[A].y*n.y+F[A].z*n.z+F[A].w;if(s<=E){s=false;break a}}s=true}s=!s}s=s}if(!s){d=
|
||||
g[e]=g[e]||new THREE.RenderableObject;J.copy(v.position);p.multiplyVector3(J);d.object=v;d.z=J.z;m.push(d);e++}}C&&m.sort(a);return m};this.projectScene=function(l,r,C){var m=[],t=r.near,v=r.far,s,n,E,A,O,N,G,W,P,I,L,V,S,w,M,Q;b=y=x=0;r.autoUpdateMatrix&&r.updateMatrix();p.multiply(r.projectionMatrix,r.matrix);N=this.projectObjects(l,r,true);l=0;for(s=N.length;l<s;l++){G=N[l].object;if(G.visible){G.autoUpdateMatrix&&G.updateMatrix();W=G.matrix;P=G.rotationMatrix;I=G.materials;L=G.overdraw;if(G instanceof
|
||||
THREE.Mesh){V=G.geometry;S=V.vertices;n=0;for(E=S.length;n<E;n++){w=S[n];w.positionWorld.copy(w.position);W.multiplyVector3(w.positionWorld);A=w.positionScreen;A.copy(w.positionWorld);p.multiplyVector4(A);A.x/=A.w;A.y/=A.w;w.__visible=A.z>t&&A.z<v}V=V.faces;n=0;for(E=V.length;n<E;n++){w=V[n];if(w instanceof THREE.Face3){A=S[w.a];O=S[w.b];M=S[w.c];if(A.__visible&&O.__visible&&M.__visible)if(G.doubleSided||G.flipSided!=(M.positionScreen.x-A.positionScreen.x)*(O.positionScreen.y-A.positionScreen.y)-
|
||||
(M.positionScreen.y-A.positionScreen.y)*(O.positionScreen.x-A.positionScreen.x)<0){h=i[b]=i[b]||new THREE.RenderableFace3;h.v1.positionWorld.copy(A.positionWorld);h.v2.positionWorld.copy(O.positionWorld);h.v3.positionWorld.copy(M.positionWorld);h.v1.positionScreen.copy(A.positionScreen);h.v2.positionScreen.copy(O.positionScreen);h.v3.positionScreen.copy(M.positionScreen);h.normalWorld.copy(w.normal);P.multiplyVector3(h.normalWorld);h.centroidWorld.copy(w.centroid);W.multiplyVector3(h.centroidWorld);
|
||||
h.centroidScreen.copy(h.centroidWorld);p.multiplyVector3(h.centroidScreen);M=w.vertexNormals;q=h.vertexNormalsWorld;A=0;for(O=M.length;A<O;A++){Q=q[A]=q[A]||new THREE.Vector3;Q.copy(M[A]);P.multiplyVector3(Q)}h.z=h.centroidScreen.z;h.meshMaterials=I;h.faceMaterials=w.materials;h.overdraw=L;if(G.geometry.uvs[n]){h.uvs[0]=G.geometry.uvs[n][0];h.uvs[1]=G.geometry.uvs[n][1];h.uvs[2]=G.geometry.uvs[n][2]}m.push(h);b++}}else if(w instanceof THREE.Face4){A=S[w.a];O=S[w.b];M=S[w.c];Q=S[w.d];if(A.__visible&&
|
||||
O.__visible&&M.__visible&&Q.__visible)if(G.doubleSided||G.flipSided!=((Q.positionScreen.x-A.positionScreen.x)*(O.positionScreen.y-A.positionScreen.y)-(Q.positionScreen.y-A.positionScreen.y)*(O.positionScreen.x-A.positionScreen.x)<0||(O.positionScreen.x-M.positionScreen.x)*(Q.positionScreen.y-M.positionScreen.y)-(O.positionScreen.y-M.positionScreen.y)*(Q.positionScreen.x-M.positionScreen.x)<0)){h=i[b]=i[b]||new THREE.RenderableFace3;h.v1.positionWorld.copy(A.positionWorld);h.v2.positionWorld.copy(O.positionWorld);
|
||||
h.v3.positionWorld.copy(Q.positionWorld);h.v1.positionScreen.copy(A.positionScreen);h.v2.positionScreen.copy(O.positionScreen);h.v3.positionScreen.copy(Q.positionScreen);h.normalWorld.copy(w.normal);P.multiplyVector3(h.normalWorld);h.centroidWorld.copy(w.centroid);W.multiplyVector3(h.centroidWorld);h.centroidScreen.copy(h.centroidWorld);p.multiplyVector3(h.centroidScreen);h.z=h.centroidScreen.z;h.meshMaterials=I;h.faceMaterials=w.materials;h.overdraw=L;if(G.geometry.uvs[n]){h.uvs[0]=G.geometry.uvs[n][0];
|
||||
h.uvs[1]=G.geometry.uvs[n][1];h.uvs[2]=G.geometry.uvs[n][3]}m.push(h);b++;o=i[b]=i[b]||new THREE.RenderableFace3;o.v1.positionWorld.copy(O.positionWorld);o.v2.positionWorld.copy(M.positionWorld);o.v3.positionWorld.copy(Q.positionWorld);o.v1.positionScreen.copy(O.positionScreen);o.v2.positionScreen.copy(M.positionScreen);o.v3.positionScreen.copy(Q.positionScreen);o.normalWorld.copy(h.normalWorld);o.centroidWorld.copy(h.centroidWorld);o.centroidScreen.copy(h.centroidScreen);o.z=o.centroidScreen.z;o.meshMaterials=
|
||||
I;o.faceMaterials=w.materials;o.overdraw=L;if(G.geometry.uvs[n]){o.uvs[0]=G.geometry.uvs[n][1];o.uvs[1]=G.geometry.uvs[n][2];o.uvs[2]=G.geometry.uvs[n][3]}m.push(o);b++}}}}else if(G instanceof THREE.Line){U.multiply(p,W);S=G.geometry.vertices;w=S[0];w.positionScreen.copy(w.position);U.multiplyVector4(w.positionScreen);n=1;for(E=S.length;n<E;n++){A=S[n];A.positionScreen.copy(A.position);U.multiplyVector4(A.positionScreen);O=S[n-1];f.copy(A.positionScreen);j.copy(O.positionScreen);if(c(f,j)){f.multiplyScalar(1/
|
||||
f.w);j.multiplyScalar(1/j.w);k=z[y]=z[y]||new THREE.RenderableLine;k.v1.positionScreen.copy(f);k.v2.positionScreen.copy(j);k.z=Math.max(f.z,j.z);k.materials=G.materials;m.push(k);y++}}}else if(G instanceof THREE.Particle){K.set(G.position.x,G.position.y,G.position.z,1);p.multiplyVector4(K);K.z/=K.w;if(K.z>0&&K.z<1){u=H[x]=H[x]||new THREE.RenderableParticle;u.x=K.x/K.w;u.y=K.y/K.w;u.z=K.z;u.rotation=G.rotation.z;u.scale.x=G.scale.x*Math.abs(u.x-(K.x+r.projectionMatrix.n11)/(K.w+r.projectionMatrix.n14));
|
||||
u.scale.y=G.scale.y*Math.abs(u.y-(K.y+r.projectionMatrix.n22)/(K.w+r.projectionMatrix.n24));u.materials=G.materials;m.push(u);x++}}}}C&&m.sort(a);return m};this.unprojectVector=function(l,r){var C=THREE.Matrix4.makeInvert(r.matrix);C.multiplySelf(THREE.Matrix4.makeInvert(r.projectionMatrix));C.multiplyVector3(l);return l}};
|
||||
THREE.DOMRenderer=function(){THREE.Renderer.call(this);var a=null,c=new THREE.Projector,d,e,g,h;this.domElement=document.createElement("div");this.setSize=function(o,b){d=o;e=b;g=d/2;h=e/2};this.render=function(o,b){var i,k,y,z,u,x,H,J;a=c.projectScene(o,b);i=0;for(k=a.length;i<k;i++){u=a[i];if(u instanceof THREE.RenderableParticle){H=u.x*g+g;J=u.y*h+h;y=0;for(z=u.material.length;y<z;y++){x=u.material[y];if(x instanceof THREE.ParticleDOMMaterial){x=x.domElement;x.style.left=H+"px";x.style.top=J+"px"}}}}}};
|
||||
THREE.CanvasRenderer=function(){function a(ea){if(u!=ea)k.globalAlpha=u=ea}function c(ea){if(x!=ea){switch(ea){case THREE.NormalBlending:k.globalCompositeOperation="source-over";break;case THREE.AdditiveBlending:k.globalCompositeOperation="lighter";break;case THREE.SubtractiveBlending:k.globalCompositeOperation="darker"}x=ea}}var d=null,e=new THREE.Projector,g=document.createElement("canvas"),h,o,b,i,k=g.getContext("2d"),y=new THREE.Color(0),z=0,u=1,x=0,H=null,J=null,K=1,p,U,F,f,j,q,l,r,C,m=new THREE.Color,
|
||||
t=new THREE.Color,v=new THREE.Color,s=new THREE.Color,n=new THREE.Color,E,A,O,N,G,W,P,I,L,V=new THREE.Rectangle,S=new THREE.Rectangle,w=new THREE.Rectangle,M=false,Q=new THREE.Color,da=new THREE.Color,ba=new THREE.Color,Z=new THREE.Color,ja=Math.PI*2,Y=new THREE.Vector3,qa,ka,fa,ha,sa,ua,va=16;qa=document.createElement("canvas");qa.width=qa.height=2;ka=qa.getContext("2d");ka.fillStyle="rgba(0,0,0,1)";ka.fillRect(0,0,2,2);fa=ka.getImageData(0,0,2,2);ha=fa.data;sa=document.createElement("canvas");sa.width=
|
||||
sa.height=va;ua=sa.getContext("2d");ua.translate(-va/2,-va/2);ua.scale(va,va);va--;this.domElement=g;this.sortElements=this.sortObjects=this.autoClear=true;this.setSize=function(ea,ra){h=ea;o=ra;b=h/2;i=o/2;g.width=h;g.height=o;V.set(-b,-i,b,i);u=1;x=0;J=H=null;K=1};this.setClearColor=function(ea,ra){y.setHex(ea);z=ra;S.set(-b,-i,b,i);k.setTransform(1,0,0,-1,b,i);this.clear()};this.clear=function(){if(!S.isEmpty()){S.inflate(1);S.minSelf(V);if(y.hex==0&&z==0)k.clearRect(S.getX(),S.getY(),S.getWidth(),
|
||||
S.getHeight());else{c(THREE.NormalBlending);a(1);k.fillStyle="rgba("+Math.floor(y.r*255)+","+Math.floor(y.g*255)+","+Math.floor(y.b*255)+","+z+")";k.fillRect(S.getX(),S.getY(),S.getWidth(),S.getHeight())}S.empty()}};this.render=function(ea,ra){function Ma(B){var X,T,D,R=B.lights;da.setRGB(0,0,0);ba.setRGB(0,0,0);Z.setRGB(0,0,0);B=0;for(X=R.length;B<X;B++){T=R[B];D=T.color;if(T instanceof THREE.AmbientLight){da.r+=D.r;da.g+=D.g;da.b+=D.b}else if(T instanceof THREE.DirectionalLight){ba.r+=D.r;ba.g+=
|
||||
D.g;ba.b+=D.b}else if(T instanceof THREE.PointLight){Z.r+=D.r;Z.g+=D.g;Z.b+=D.b}}}function Aa(B,X,T,D){var R,$,ca,ga,ia=B.lights;B=0;for(R=ia.length;B<R;B++){$=ia[B];ca=$.color;ga=$.intensity;if($ instanceof THREE.DirectionalLight){$=T.dot($.position)*ga;if($>0){D.r+=ca.r*$;D.g+=ca.g*$;D.b+=ca.b*$}}else if($ instanceof THREE.PointLight){Y.sub($.position,X);Y.normalize();$=T.dot(Y)*ga;if($>0){D.r+=ca.r*$;D.g+=ca.g*$;D.b+=ca.b*$}}}}function Na(B,X,T){if(T.opacity!=0){a(T.opacity);c(T.blending);var D,
|
||||
R,$,ca,ga,ia;if(T instanceof THREE.ParticleBasicMaterial){if(T.map){ca=T.map;ga=ca.width>>1;ia=ca.height>>1;R=X.scale.x*b;$=X.scale.y*i;T=R*ga;D=$*ia;w.set(B.x-T,B.y-D,B.x+T,B.y+D);if(V.instersects(w)){k.save();k.translate(B.x,B.y);k.rotate(-X.rotation);k.scale(R,-$);k.translate(-ga,-ia);k.drawImage(ca,0,0);k.restore()}}}else if(T instanceof THREE.ParticleCircleMaterial){if(M){Q.r=da.r+ba.r+Z.r;Q.g=da.g+ba.g+Z.g;Q.b=da.b+ba.b+Z.b;m.r=T.color.r*Q.r;m.g=T.color.g*Q.g;m.b=T.color.b*Q.b;m.updateStyleString()}else m.__styleString=
|
||||
T.color.__styleString;T=X.scale.x*b;D=X.scale.y*i;w.set(B.x-T,B.y-D,B.x+T,B.y+D);if(V.instersects(w)){R=m.__styleString;if(J!=R)k.fillStyle=J=R;k.save();k.translate(B.x,B.y);k.rotate(-X.rotation);k.scale(T,D);k.beginPath();k.arc(0,0,1,0,ja,true);k.closePath();k.fill();k.restore()}}}}function Oa(B,X,T,D){if(D.opacity!=0){a(D.opacity);c(D.blending);k.beginPath();k.moveTo(B.positionScreen.x,B.positionScreen.y);k.lineTo(X.positionScreen.x,X.positionScreen.y);k.closePath();if(D instanceof THREE.LineBasicMaterial){m.__styleString=
|
||||
D.color.__styleString;B=D.linewidth;if(K!=B)k.lineWidth=K=B;B=m.__styleString;if(H!=B)k.strokeStyle=H=B;k.stroke();w.inflate(D.linewidth*2)}}}function Ia(B,X,T,D,R,$){if(R.opacity!=0){a(R.opacity);c(R.blending);f=B.positionScreen.x;j=B.positionScreen.y;q=X.positionScreen.x;l=X.positionScreen.y;r=T.positionScreen.x;C=T.positionScreen.y;k.beginPath();k.moveTo(f,j);k.lineTo(q,l);k.lineTo(r,C);k.lineTo(f,j);k.closePath();if(R instanceof THREE.MeshBasicMaterial)if(R.map)R.map.image.loaded&&R.map.mapping instanceof
|
||||
THREE.UVMapping&&xa(f,j,q,l,r,C,R.map.image,D.uvs[0].u,D.uvs[0].v,D.uvs[1].u,D.uvs[1].v,D.uvs[2].u,D.uvs[2].v);else if(R.env_map){if(R.env_map.image.loaded)if(R.env_map.mapping instanceof THREE.SphericalReflectionMapping){B=ra.matrix;Y.copy(D.vertexNormalsWorld[0]);N=(Y.x*B.n11+Y.y*B.n12+Y.z*B.n13)*0.5+0.5;G=-(Y.x*B.n21+Y.y*B.n22+Y.z*B.n23)*0.5+0.5;Y.copy(D.vertexNormalsWorld[1]);W=(Y.x*B.n11+Y.y*B.n12+Y.z*B.n13)*0.5+0.5;P=-(Y.x*B.n21+Y.y*B.n22+Y.z*B.n23)*0.5+0.5;Y.copy(D.vertexNormalsWorld[2]);I=
|
||||
(Y.x*B.n11+Y.y*B.n12+Y.z*B.n13)*0.5+0.5;L=-(Y.x*B.n21+Y.y*B.n22+Y.z*B.n23)*0.5+0.5;xa(f,j,q,l,r,C,R.env_map.image,N,G,W,P,I,L)}}else R.wireframe?Ba(R.color.__styleString,R.wireframe_linewidth):Ca(R.color.__styleString);else if(R instanceof THREE.MeshLambertMaterial){if(R.map&&!R.wireframe){R.map.mapping instanceof THREE.UVMapping&&xa(f,j,q,l,r,C,R.map.image,D.uvs[0].u,D.uvs[0].v,D.uvs[1].u,D.uvs[1].v,D.uvs[2].u,D.uvs[2].v);c(THREE.SubtractiveBlending)}if(M)if(!R.wireframe&&R.shading==THREE.SmoothShading&&
|
||||
D.vertexNormalsWorld.length==3){t.r=v.r=s.r=da.r;t.g=v.g=s.g=da.g;t.b=v.b=s.b=da.b;Aa($,D.v1.positionWorld,D.vertexNormalsWorld[0],t);Aa($,D.v2.positionWorld,D.vertexNormalsWorld[1],v);Aa($,D.v3.positionWorld,D.vertexNormalsWorld[2],s);n.r=(v.r+s.r)*0.5;n.g=(v.g+s.g)*0.5;n.b=(v.b+s.b)*0.5;O=Ja(t,v,s,n);xa(f,j,q,l,r,C,O,0,0,1,0,0,1)}else{Q.r=da.r;Q.g=da.g;Q.b=da.b;Aa($,D.centroidWorld,D.normalWorld,Q);m.r=R.color.r*Q.r;m.g=R.color.g*Q.g;m.b=R.color.b*Q.b;m.updateStyleString();R.wireframe?Ba(m.__styleString,
|
||||
R.wireframe_linewidth):Ca(m.__styleString)}else R.wireframe?Ba(R.color.__styleString,R.wireframe_linewidth):Ca(R.color.__styleString)}else if(R instanceof THREE.MeshDepthMaterial){E=ra.near;A=ra.far;t.r=t.g=t.b=1-Ea(B.positionScreen.z,E,A);v.r=v.g=v.b=1-Ea(X.positionScreen.z,E,A);s.r=s.g=s.b=1-Ea(T.positionScreen.z,E,A);n.r=(v.r+s.r)*0.5;n.g=(v.g+s.g)*0.5;n.b=(v.b+s.b)*0.5;O=Ja(t,v,s,n);xa(f,j,q,l,r,C,O,0,0,1,0,0,1)}else if(R instanceof THREE.MeshNormalMaterial){m.r=Fa(D.normalWorld.x);m.g=Fa(D.normalWorld.y);
|
||||
m.b=Fa(D.normalWorld.z);m.updateStyleString();R.wireframe?Ba(m.__styleString,R.wireframe_linewidth):Ca(m.__styleString)}}}function Ba(B,X){if(H!=B)k.strokeStyle=H=B;if(K!=X)k.lineWidth=K=X;k.stroke();w.inflate(X*2)}function Ca(B){if(J!=B)k.fillStyle=J=B;k.fill()}function xa(B,X,T,D,R,$,ca,ga,ia,na,la,oa,ya){var ta,pa;ta=ca.width-1;pa=ca.height-1;ga*=ta;ia*=pa;na*=ta;la*=pa;oa*=ta;ya*=pa;T-=B;D-=X;R-=B;$-=X;na-=ga;la-=ia;oa-=ga;ya-=ia;pa=1/(na*ya-oa*la);ta=(ya*T-la*R)*pa;la=(ya*D-la*$)*pa;T=(na*R-
|
||||
oa*T)*pa;D=(na*$-oa*D)*pa;B=B-ta*ga-T*ia;X=X-la*ga-D*ia;k.save();k.transform(ta,la,T,D,B,X);k.clip();k.drawImage(ca,0,0);k.restore()}function Ja(B,X,T,D){var R=~~(B.r*255),$=~~(B.g*255);B=~~(B.b*255);var ca=~~(X.r*255),ga=~~(X.g*255);X=~~(X.b*255);var ia=~~(T.r*255),na=~~(T.g*255);T=~~(T.b*255);var la=~~(D.r*255),oa=~~(D.g*255);D=~~(D.b*255);ha[0]=R<0?0:R>255?255:R;ha[1]=$<0?0:$>255?255:$;ha[2]=B<0?0:B>255?255:B;ha[4]=ca<0?0:ca>255?255:ca;ha[5]=ga<0?0:ga>255?255:ga;ha[6]=X<0?0:X>255?255:X;ha[8]=ia<
|
||||
0?0:ia>255?255:ia;ha[9]=na<0?0:na>255?255:na;ha[10]=T<0?0:T>255?255:T;ha[12]=la<0?0:la>255?255:la;ha[13]=oa<0?0:oa>255?255:oa;ha[14]=D<0?0:D>255?255:D;ka.putImageData(fa,0,0);ua.drawImage(qa,0,0);return sa}function Ea(B,X,T){B=(B-X)/(T-X);return B*B*(3-2*B)}function Fa(B){B=(B+1)*0.5;return B<0?0:B>1?1:B}function Ga(B,X){var T=X.x-B.x,D=X.y-B.y,R=1/Math.sqrt(T*T+D*D);T*=R;D*=R;X.x+=T;X.y+=D;B.x-=T;B.y-=D}var Da,Ka,aa,ma,wa,Ha,La,za;k.setTransform(1,0,0,-1,b,i);this.autoClear&&this.clear();d=e.projectScene(ea,
|
||||
ra,this.sortElements);(M=ea.lights.length>0)&&Ma(ea);Da=0;for(Ka=d.length;Da<Ka;Da++){aa=d[Da];w.empty();if(aa instanceof THREE.RenderableParticle){p=aa;p.x*=b;p.y*=i;ma=0;for(wa=aa.materials.length;ma<wa;ma++)Na(p,aa,aa.materials[ma],ea)}else if(aa instanceof THREE.RenderableLine){p=aa.v1;U=aa.v2;p.positionScreen.x*=b;p.positionScreen.y*=i;U.positionScreen.x*=b;U.positionScreen.y*=i;w.addPoint(p.positionScreen.x,p.positionScreen.y);w.addPoint(U.positionScreen.x,U.positionScreen.y);if(V.instersects(w)){ma=
|
||||
0;for(wa=aa.materials.length;ma<wa;)Oa(p,U,aa,aa.materials[ma++],ea)}}else if(aa instanceof THREE.RenderableFace3){p=aa.v1;U=aa.v2;F=aa.v3;p.positionScreen.x*=b;p.positionScreen.y*=i;U.positionScreen.x*=b;U.positionScreen.y*=i;F.positionScreen.x*=b;F.positionScreen.y*=i;if(aa.overdraw){Ga(p.positionScreen,U.positionScreen);Ga(U.positionScreen,F.positionScreen);Ga(F.positionScreen,p.positionScreen)}w.add3Points(p.positionScreen.x,p.positionScreen.y,U.positionScreen.x,U.positionScreen.y,F.positionScreen.x,
|
||||
F.positionScreen.y);if(V.instersects(w)){ma=0;for(wa=aa.meshMaterials.length;ma<wa;){za=aa.meshMaterials[ma++];if(za instanceof THREE.MeshFaceMaterial){Ha=0;for(La=aa.faceMaterials.length;Ha<La;)(za=aa.faceMaterials[Ha++])&&Ia(p,U,F,aa,za,ea)}else Ia(p,U,F,aa,za,ea)}}}S.addRectangle(w)}k.setTransform(1,0,0,1,0,0)}};
|
||||
THREE.SVGRenderer=function(){function a(N,G,W){var P,I,L,V;P=0;for(I=N.lights.length;P<I;P++){L=N.lights[P];if(L instanceof THREE.DirectionalLight){V=G.normalWorld.dot(L.position)*L.intensity;if(V>0){W.r+=L.color.r*V;W.g+=L.color.g*V;W.b+=L.color.b*V}}else if(L instanceof THREE.PointLight){C.sub(L.position,G.centroidWorld);C.normalize();V=G.normalWorld.dot(C)*L.intensity;if(V>0){W.r+=L.color.r*V;W.g+=L.color.g*V;W.b+=L.color.b*V}}}}function c(N,G,W,P,I,L){s=e(n++);s.setAttribute("d","M "+N.positionScreen.x+
|
||||
" "+N.positionScreen.y+" L "+G.positionScreen.x+" "+G.positionScreen.y+" L "+W.positionScreen.x+","+W.positionScreen.y+"z");if(I instanceof THREE.MeshBasicMaterial)F.__styleString=I.color.__styleString;else if(I instanceof THREE.MeshLambertMaterial)if(U){f.r=j.r;f.g=j.g;f.b=j.b;a(L,P,f);F.r=I.color.r*f.r;F.g=I.color.g*f.g;F.b=I.color.b*f.b;F.updateStyleString()}else F.__styleString=I.color.__styleString;else if(I instanceof THREE.MeshDepthMaterial){r=1-I.__2near/(I.__farPlusNear-P.z*I.__farMinusNear);
|
||||
F.setRGB(r,r,r)}else I instanceof THREE.MeshNormalMaterial&&F.setRGB(g(P.normalWorld.x),g(P.normalWorld.y),g(P.normalWorld.z));I.wireframe?s.setAttribute("style","fill: none; stroke: "+F.__styleString+"; stroke-width: "+I.wireframe_linewidth+"; stroke-opacity: "+I.opacity+"; stroke-linecap: "+I.wireframe_linecap+"; stroke-linejoin: "+I.wireframe_linejoin):s.setAttribute("style","fill: "+F.__styleString+"; fill-opacity: "+I.opacity);b.appendChild(s)}function d(N,G,W,P,I,L,V){s=e(n++);s.setAttribute("d",
|
||||
"M "+N.positionScreen.x+" "+N.positionScreen.y+" L "+G.positionScreen.x+" "+G.positionScreen.y+" L "+W.positionScreen.x+","+W.positionScreen.y+" L "+P.positionScreen.x+","+P.positionScreen.y+"z");if(L instanceof THREE.MeshBasicMaterial)F.__styleString=L.color.__styleString;else if(L instanceof THREE.MeshLambertMaterial)if(U){f.r=j.r;f.g=j.g;f.b=j.b;a(V,I,f);F.r=L.color.r*f.r;F.g=L.color.g*f.g;F.b=L.color.b*f.b;F.updateStyleString()}else F.__styleString=L.color.__styleString;else if(L instanceof THREE.MeshDepthMaterial){r=
|
||||
1-L.__2near/(L.__farPlusNear-I.z*L.__farMinusNear);F.setRGB(r,r,r)}else L instanceof THREE.MeshNormalMaterial&&F.setRGB(g(I.normalWorld.x),g(I.normalWorld.y),g(I.normalWorld.z));L.wireframe?s.setAttribute("style","fill: none; stroke: "+F.__styleString+"; stroke-width: "+L.wireframe_linewidth+"; stroke-opacity: "+L.opacity+"; stroke-linecap: "+L.wireframe_linecap+"; stroke-linejoin: "+L.wireframe_linejoin):s.setAttribute("style","fill: "+F.__styleString+"; fill-opacity: "+L.opacity);b.appendChild(s)}
|
||||
function e(N){if(m[N]==null){m[N]=document.createElementNS("http://www.w3.org/2000/svg","path");O==0&&m[N].setAttribute("shape-rendering","crispEdges");return m[N]}return m[N]}function g(N){return N<0?Math.min((1+N)*0.5,0.5):0.5+Math.min(N*0.5,0.5)}var h=null,o=new THREE.Projector,b=document.createElementNS("http://www.w3.org/2000/svg","svg"),i,k,y,z,u,x,H,J,K=new THREE.Rectangle,p=new THREE.Rectangle,U=false,F=new THREE.Color(16777215),f=new THREE.Color(16777215),j=new THREE.Color(0),q=new THREE.Color(0),
|
||||
l=new THREE.Color(0),r,C=new THREE.Vector3,m=[],t=[],v=[],s,n,E,A,O=1;this.domElement=b;this.sortElements=this.sortObjects=this.autoClear=true;this.setQuality=function(N){switch(N){case "high":O=1;break;case "low":O=0}};this.setSize=function(N,G){i=N;k=G;y=i/2;z=k/2;b.setAttribute("viewBox",-y+" "+-z+" "+i+" "+k);b.setAttribute("width",i);b.setAttribute("height",k);K.set(-y,-z,y,z)};this.clear=function(){for(;b.childNodes.length>0;)b.removeChild(b.childNodes[0])};this.render=function(N,G){var W,P,
|
||||
I,L,V,S,w,M;this.autoClear&&this.clear();h=o.projectScene(N,G,this.sortElements);A=E=n=0;if(U=N.lights.length>0){w=N.lights;j.setRGB(0,0,0);q.setRGB(0,0,0);l.setRGB(0,0,0);W=0;for(P=w.length;W<P;W++){I=w[W];L=I.color;if(I instanceof THREE.AmbientLight){j.r+=L.r;j.g+=L.g;j.b+=L.b}else if(I instanceof THREE.DirectionalLight){q.r+=L.r;q.g+=L.g;q.b+=L.b}else if(I instanceof THREE.PointLight){l.r+=L.r;l.g+=L.g;l.b+=L.b}}}W=0;for(P=h.length;W<P;W++){w=h[W];p.empty();if(w instanceof THREE.RenderableParticle){u=
|
||||
w;u.x*=y;u.y*=-z;I=0;for(L=w.materials.length;I<L;I++)if(M=w.materials[I]){V=u;S=w;M=M;var Q=E++;if(t[Q]==null){t[Q]=document.createElementNS("http://www.w3.org/2000/svg","circle");O==0&&t[Q].setAttribute("shape-rendering","crispEdges")}s=t[Q];s.setAttribute("cx",V.x);s.setAttribute("cy",V.y);s.setAttribute("r",S.scale.x*y);if(M instanceof THREE.ParticleCircleMaterial){if(U){f.r=j.r+q.r+l.r;f.g=j.g+q.g+l.g;f.b=j.b+q.b+l.b;F.r=M.color.r*f.r;F.g=M.color.g*f.g;F.b=M.color.b*f.b;F.updateStyleString()}else F=
|
||||
M.color;s.setAttribute("style","fill: "+F.__styleString)}b.appendChild(s)}}else if(w instanceof THREE.RenderableLine){u=w.v1;x=w.v2;u.positionScreen.x*=y;u.positionScreen.y*=-z;x.positionScreen.x*=y;x.positionScreen.y*=-z;p.addPoint(u.positionScreen.x,u.positionScreen.y);p.addPoint(x.positionScreen.x,x.positionScreen.y);if(K.instersects(p)){I=0;for(L=w.materials.length;I<L;)if(M=w.materials[I++]){V=u;S=x;M=M;Q=A++;if(v[Q]==null){v[Q]=document.createElementNS("http://www.w3.org/2000/svg","line");O==
|
||||
0&&v[Q].setAttribute("shape-rendering","crispEdges")}s=v[Q];s.setAttribute("x1",V.positionScreen.x);s.setAttribute("y1",V.positionScreen.y);s.setAttribute("x2",S.positionScreen.x);s.setAttribute("y2",S.positionScreen.y);if(M instanceof THREE.LineBasicMaterial){F.__styleString=M.color.__styleString;s.setAttribute("style","fill: none; stroke: "+F.__styleString+"; stroke-width: "+M.linewidth+"; stroke-opacity: "+M.opacity+"; stroke-linecap: "+M.linecap+"; stroke-linejoin: "+M.linejoin);b.appendChild(s)}}}}else if(w instanceof
|
||||
THREE.RenderableFace3){u=w.v1;x=w.v2;H=w.v3;u.positionScreen.x*=y;u.positionScreen.y*=-z;x.positionScreen.x*=y;x.positionScreen.y*=-z;H.positionScreen.x*=y;H.positionScreen.y*=-z;p.addPoint(u.positionScreen.x,u.positionScreen.y);p.addPoint(x.positionScreen.x,x.positionScreen.y);p.addPoint(H.positionScreen.x,H.positionScreen.y);if(K.instersects(p)){I=0;for(L=w.meshMaterials.length;I<L;){M=w.meshMaterials[I++];if(M instanceof THREE.MeshFaceMaterial){V=0;for(S=w.faceMaterials.length;V<S;)(M=w.faceMaterials[V++])&&
|
||||
c(u,x,H,w,M,N)}else M&&c(u,x,H,w,M,N)}}}else if(w instanceof THREE.RenderableFace4){u=w.v1;x=w.v2;H=w.v3;J=w.v4;u.positionScreen.x*=y;u.positionScreen.y*=-z;x.positionScreen.x*=y;x.positionScreen.y*=-z;H.positionScreen.x*=y;H.positionScreen.y*=-z;J.positionScreen.x*=y;J.positionScreen.y*=-z;p.addPoint(u.positionScreen.x,u.positionScreen.y);p.addPoint(x.positionScreen.x,x.positionScreen.y);p.addPoint(H.positionScreen.x,H.positionScreen.y);p.addPoint(J.positionScreen.x,J.positionScreen.y);if(K.instersects(p)){I=
|
||||
0;for(L=w.meshMaterials.length;I<L;){M=w.meshMaterials[I++];if(M instanceof THREE.MeshFaceMaterial){V=0;for(S=w.faceMaterials.length;V<S;)(M=w.faceMaterials[V++])&&d(u,x,H,J,w,M,N)}else M&&d(u,x,H,J,w,M,N)}}}}}};
|
||||
THREE.WebGLRenderer=function(a){function c(f,j){f.fragment_shader=j.fragment_shader;f.vertex_shader=j.vertex_shader;f.uniforms=Uniforms.clone(j.uniforms)}function d(f,j){f.uniforms.color.value.setRGB(f.color.r*f.opacity,f.color.g*f.opacity,f.color.b*f.opacity);f.uniforms.opacity.value=f.opacity;f.uniforms.map.texture=f.map;f.uniforms.env_map.texture=f.env_map;f.uniforms.reflectivity.value=f.reflectivity;f.uniforms.refraction_ratio.value=f.refraction_ratio;f.uniforms.combine.value=f.combine;f.uniforms.useRefract.value=
|
||||
f.env_map&&f.env_map.mapping instanceof THREE.CubeRefractionMapping;if(j){f.uniforms.fogColor.value.setHex(j.color.hex);if(j instanceof THREE.Fog){f.uniforms.fogNear.value=j.near;f.uniforms.fogFar.value=j.far}else if(j instanceof THREE.FogExp2)f.uniforms.fogDensity.value=j.density}}function e(f,j){f.uniforms.color.value.setRGB(f.color.r*f.opacity,f.color.g*f.opacity,f.color.b*f.opacity);f.uniforms.opacity.value=f.opacity;if(j){f.uniforms.fogColor.value.setHex(j.color.hex);if(j instanceof THREE.Fog){f.uniforms.fogNear.value=
|
||||
j.near;f.uniforms.fogFar.value=j.far}else if(j instanceof THREE.FogExp2)f.uniforms.fogDensity.value=j.density}}function g(f,j){var q;if(f=="fragment")q=b.createShader(b.FRAGMENT_SHADER);else if(f=="vertex")q=b.createShader(b.VERTEX_SHADER);b.shaderSource(q,j);b.compileShader(q);if(!b.getShaderParameter(q,b.COMPILE_STATUS)){alert(b.getShaderInfoLog(q));return null}return q}function h(f){switch(f){case THREE.RepeatWrapping:return b.REPEAT;case THREE.ClampToEdgeWrapping:return b.CLAMP_TO_EDGE;case THREE.MirroredRepeatWrapping:return b.MIRRORED_REPEAT;
|
||||
case THREE.NearestFilter:return b.NEAREST;case THREE.NearestMipMapNearestFilter:return b.NEAREST_MIPMAP_NEAREST;case THREE.NearestMipMapLinearFilter:return b.NEAREST_MIPMAP_LINEAR;case THREE.LinearFilter:return b.LINEAR;case THREE.LinearMipMapNearestFilter:return b.LINEAR_MIPMAP_NEAREST;case THREE.LinearMipMapLinearFilter:return b.LINEAR_MIPMAP_LINEAR;case THREE.ByteType:return b.BYTE;case THREE.UnsignedByteType:return b.UNSIGNED_BYTE;case THREE.ShortType:return b.SHORT;case THREE.UnsignedShortType:return b.UNSIGNED_SHORT;
|
||||
case THREE.IntType:return b.INT;case THREE.UnsignedShortType:return b.UNSIGNED_INT;case THREE.FloatType:return b.FLOAT;case THREE.AlphaFormat:return b.ALPHA;case THREE.RGBFormat:return b.RGB;case THREE.RGBAFormat:return b.RGBA;case THREE.LuminanceFormat:return b.LUMINANCE;case THREE.LuminanceAlphaFormat:return b.LUMINANCE_ALPHA}return 0}var o=document.createElement("canvas"),b,i=null,k=null,y=new THREE.Matrix4,z,u=new Float32Array(16),x=new Float32Array(16),H=new Float32Array(16),J=new Float32Array(9),
|
||||
K=new Float32Array(16),p=true,U=new THREE.Color(0),F=0;if(a){if(a.antialias!==undefined)p=a.antialias;a.clearColor!==undefined&&U.setHex(a.clearColor);if(a.clearAlpha!==undefined)F=a.clearAlpha}this.domElement=o;this.autoClear=true;(function(f,j,q){try{b=o.getContext("experimental-webgl",{antialias:f})}catch(l){}if(!b){alert("WebGL not supported");throw"cannot create webgl context";}b.clearColor(0,0,0,1);b.clearDepth(1);b.enable(b.DEPTH_TEST);b.depthFunc(b.LEQUAL);b.frontFace(b.CCW);b.cullFace(b.BACK);
|
||||
b.enable(b.CULL_FACE);b.enable(b.BLEND);b.blendFunc(b.ONE,b.ONE_MINUS_SRC_ALPHA);b.clearColor(j.r,j.g,j.b,q)})(p,U,F);this.context=b;this.lights={ambient:[0,0,0],directional:{length:0,colors:[],positions:[]},point:{length:0,colors:[],positions:[]}};this.setSize=function(f,j){o.width=f;o.height=j;b.viewport(0,0,o.width,o.height)};this.setClearColor=function(f,j){var q=new THREE.Color(f);b.clearColor(q.r,q.g,q.b,j)};this.clear=function(){b.clear(b.COLOR_BUFFER_BIT|b.DEPTH_BUFFER_BIT)};this.setupLights=
|
||||
function(f,j){var q,l,r,C=0,m=0,t=0,v,s,n,E=this.lights,A=E.directional.colors,O=E.directional.positions,N=E.point.colors,G=E.point.positions,W=0,P=0;q=0;for(l=j.length;q<l;q++){r=j[q];v=r.color;s=r.position;n=r.intensity;if(r instanceof THREE.AmbientLight){C+=v.r;m+=v.g;t+=v.b}else if(r instanceof THREE.DirectionalLight){A[W*3]=v.r*n;A[W*3+1]=v.g*n;A[W*3+2]=v.b*n;O[W*3]=s.x;O[W*3+1]=s.y;O[W*3+2]=s.z;W+=1}else if(r instanceof THREE.PointLight){N[P*3]=v.r*n;N[P*3+1]=v.g*n;N[P*3+2]=v.b*n;G[P*3]=s.x;
|
||||
G[P*3+1]=s.y;G[P*3+2]=s.z;P+=1}}E.point.length=P;E.directional.length=W;E.ambient[0]=C;E.ambient[1]=m;E.ambient[2]=t};this.createParticleBuffers=function(f){f.__webGLVertexBuffer=b.createBuffer();f.__webGLFaceBuffer=b.createBuffer()};this.createLineBuffers=function(f){f.__webGLVertexBuffer=b.createBuffer();f.__webGLLineBuffer=b.createBuffer()};this.createMeshBuffers=function(f){f.__webGLVertexBuffer=b.createBuffer();f.__webGLNormalBuffer=b.createBuffer();f.__webGLTangentBuffer=b.createBuffer();f.__webGLUVBuffer=
|
||||
b.createBuffer();f.__webGLFaceBuffer=b.createBuffer();f.__webGLLineBuffer=b.createBuffer()};this.initLineBuffers=function(f){var j=f.vertices.length;f.__vertexArray=new Float32Array(j*3);f.__lineArray=new Uint16Array(j);f.__webGLLineCount=j};this.initMeshBuffers=function(f,j){var q,l,r=0,C=0,m=0,t=j.geometry.faces,v=f.faces;q=0;for(l=v.length;q<l;q++){fi=v[q];face=t[fi];if(face instanceof THREE.Face3){r+=3;C+=1;m+=3}else if(face instanceof THREE.Face4){r+=4;C+=2;m+=4}}f.__vertexArray=new Float32Array(r*
|
||||
3);f.__normalArray=new Float32Array(r*3);f.__tangentArray=new Float32Array(r*4);f.__uvArray=new Float32Array(r*2);f.__faceArray=new Uint16Array(C*3);f.__lineArray=new Uint16Array(m*2);r=false;q=0;for(l=j.materials.length;q<l;q++){t=j.materials[q];if(t instanceof THREE.MeshFaceMaterial){t=0;for(v=f.materials.length;t<v;t++)if(f.materials[t]&&f.materials[t].shading!=undefined&&f.materials[t].shading==THREE.SmoothShading){r=true;break}}else if(t&&t.shading!=undefined&&t.shading==THREE.SmoothShading){r=
|
||||
true;break}if(r)break}f.__needsSmoothNormals=r;f.__webGLFaceCount=C*3;f.__webGLLineCount=m*2};this.setMeshBuffers=function(f,j,q,l,r,C,m,t){var v,s,n,E,A,O,N,G,W,P=0,I=0,L=0,V=0,S=0,w=0,M=0,Q=f.__vertexArray,da=f.__uvArray,ba=f.__normalArray,Z=f.__tangentArray,ja=f.__faceArray,Y=f.__lineArray,qa=f.__needsSmoothNormals,ka=j.geometry,fa=ka.vertices,ha=f.faces,sa=ka.faces,ua=ka.uvs;j=0;for(v=ha.length;j<v;j++){s=ha[j];n=sa[s];s=ua[s];E=n.vertexNormals;A=n.normal;if(n instanceof THREE.Face3){if(l){O=
|
||||
fa[n.a].position;N=fa[n.b].position;G=fa[n.c].position;Q[I]=O.x;Q[I+1]=O.y;Q[I+2]=O.z;Q[I+3]=N.x;Q[I+4]=N.y;Q[I+5]=N.z;Q[I+6]=G.x;Q[I+7]=G.y;Q[I+8]=G.z;I+=9}if(t&&ka.hasTangents){O=fa[n.a].tangent;N=fa[n.b].tangent;G=fa[n.c].tangent;Z[w]=O.x;Z[w+1]=O.y;Z[w+2]=O.z;Z[w+3]=O.w;Z[w+4]=N.x;Z[w+5]=N.y;Z[w+6]=N.z;Z[w+7]=N.w;Z[w+8]=G.x;Z[w+9]=G.y;Z[w+10]=G.z;Z[w+11]=G.w;w+=12}if(m)if(E.length==3&&qa)for(n=0;n<3;n++){A=E[n];ba[S]=A.x;ba[S+1]=A.y;ba[S+2]=A.z;S+=3}else for(n=0;n<3;n++){ba[S]=A.x;ba[S+1]=A.y;
|
||||
ba[S+2]=A.z;S+=3}if(C&&s)for(n=0;n<3;n++){E=s[n];da[L]=E.u;da[L+1]=E.v;L+=2}if(r){ja[V]=P;ja[V+1]=P+1;ja[V+2]=P+2;V+=3;Y[M]=P;Y[M+1]=P+1;Y[M+2]=P;Y[M+3]=P+2;Y[M+4]=P+1;Y[M+5]=P+2;M+=6;P+=3}}else if(n instanceof THREE.Face4){if(l){O=fa[n.a].position;N=fa[n.b].position;G=fa[n.c].position;W=fa[n.d].position;Q[I]=O.x;Q[I+1]=O.y;Q[I+2]=O.z;Q[I+3]=N.x;Q[I+4]=N.y;Q[I+5]=N.z;Q[I+6]=G.x;Q[I+7]=G.y;Q[I+8]=G.z;Q[I+9]=W.x;Q[I+10]=W.y;Q[I+11]=W.z;I+=12}if(t&&ka.hasTangents){O=fa[n.a].tangent;N=fa[n.b].tangent;
|
||||
G=fa[n.c].tangent;n=fa[n.d].tangent;Z[w]=O.x;Z[w+1]=O.y;Z[w+2]=O.z;Z[w+3]=O.w;Z[w+4]=N.x;Z[w+5]=N.y;Z[w+6]=N.z;Z[w+7]=N.w;Z[w+8]=G.x;Z[w+9]=G.y;Z[w+10]=G.z;Z[w+11]=G.w;Z[w+12]=n.x;Z[w+13]=n.y;Z[w+14]=n.z;Z[w+15]=n.w;w+=16}if(m)if(E.length==4&&qa)for(n=0;n<4;n++){A=E[n];ba[S]=A.x;ba[S+1]=A.y;ba[S+2]=A.z;S+=3}else for(n=0;n<4;n++){ba[S]=A.x;ba[S+1]=A.y;ba[S+2]=A.z;S+=3}if(C&&s)for(n=0;n<4;n++){E=s[n];da[L]=E.u;da[L+1]=E.v;L+=2}if(r){ja[V]=P;ja[V+1]=P+1;ja[V+2]=P+2;ja[V+3]=P;ja[V+4]=P+2;ja[V+5]=P+3;
|
||||
V+=6;Y[M]=P;Y[M+1]=P+1;Y[M+2]=P;Y[M+3]=P+3;Y[M+4]=P+1;Y[M+5]=P+2;Y[M+6]=P+2;Y[M+7]=P+3;M+=8;P+=4}}}if(l){b.bindBuffer(b.ARRAY_BUFFER,f.__webGLVertexBuffer);b.bufferData(b.ARRAY_BUFFER,Q,q)}if(m){b.bindBuffer(b.ARRAY_BUFFER,f.__webGLNormalBuffer);b.bufferData(b.ARRAY_BUFFER,ba,q)}if(t&&ka.hasTangents){b.bindBuffer(b.ARRAY_BUFFER,f.__webGLTangentBuffer);b.bufferData(b.ARRAY_BUFFER,Z,q)}if(C&&L>0){b.bindBuffer(b.ARRAY_BUFFER,f.__webGLUVBuffer);b.bufferData(b.ARRAY_BUFFER,da,q)}if(r){b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,
|
||||
f.__webGLFaceBuffer);b.bufferData(b.ELEMENT_ARRAY_BUFFER,ja,q);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,f.__webGLLineBuffer);b.bufferData(b.ELEMENT_ARRAY_BUFFER,Y,q)}};this.setLineBuffers=function(f,j,q,l){var r,C,m=f.vertices,t=m.length,v=f.__vertexArray,s=f.__lineArray;if(q)for(q=0;q<t;q++){r=m[q].position;C=q*3;v[C]=r.x;v[C+1]=r.y;v[C+2]=r.z}if(l)for(q=0;q<t;q++)s[q]=q;b.bindBuffer(b.ARRAY_BUFFER,f.__webGLVertexBuffer);b.bufferData(b.ARRAY_BUFFER,v,j);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,f.__webGLLineBuffer);
|
||||
b.bufferData(b.ELEMENT_ARRAY_BUFFER,s,j)};this.setParticleBuffers=function(){};this.renderBuffer=function(f,j,q,l,r,C){var m,t,v,s;if(!l.program){if(l instanceof THREE.MeshDepthMaterial){c(l,THREE.ShaderLib.depth);l.uniforms.mNear.value=f.near;l.uniforms.mFar.value=f.far}else if(l instanceof THREE.MeshNormalMaterial)c(l,THREE.ShaderLib.normal);else if(l instanceof THREE.MeshBasicMaterial){c(l,THREE.ShaderLib.basic);d(l,q)}else if(l instanceof THREE.MeshLambertMaterial){c(l,THREE.ShaderLib.lambert);
|
||||
d(l,q)}else if(l instanceof THREE.MeshPhongMaterial){c(l,THREE.ShaderLib.phong);d(l,q)}else if(l instanceof THREE.LineBasicMaterial){c(l,THREE.ShaderLib.basic);e(l,q)}var n,E,A;n=s=t=0;for(E=j.length;n<E;n++){A=j[n];A instanceof THREE.DirectionalLight&&s++;A instanceof THREE.PointLight&&t++}if(t+s<=4){n=s;t=t}else{n=Math.ceil(4*s/(t+s));t=4-n}t={directional:n,point:t};s={fog:q,map:l.map,env_map:l.env_map,maxDirLights:t.directional,maxPointLights:t.point};t=l.fragment_shader;n=l.vertex_shader;E=b.createProgram();
|
||||
A=["#ifdef GL_ES\nprecision highp float;\n#endif","#define MAX_DIR_LIGHTS "+s.maxDirLights,"#define MAX_POINT_LIGHTS "+s.maxPointLights,s.fog?"#define USE_FOG":"",s.fog instanceof THREE.FogExp2?"#define FOG_EXP2":"",s.map?"#define USE_MAP":"",s.env_map?"#define USE_ENVMAP":"","uniform mat4 viewMatrix;\nuniform vec3 cameraPosition;\n"].join("\n");s=[b.getParameter(b.MAX_VERTEX_TEXTURE_IMAGE_UNITS)>0?"#define VERTEX_TEXTURES":"","#define MAX_DIR_LIGHTS "+s.maxDirLights,"#define MAX_POINT_LIGHTS "+s.maxPointLights,
|
||||
s.map?"#define USE_MAP":"",s.env_map?"#define USE_ENVMAP":"","uniform mat4 objectMatrix;\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 cameraPosition;\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec2 uv;\n"].join("\n");b.attachShader(E,g("fragment",A+t));b.attachShader(E,g("vertex",s+n));b.linkProgram(E);b.getProgramParameter(E,b.LINK_STATUS)||alert("Could not initialise shaders\nVALIDATE_STATUS: "+
|
||||
b.getProgramParameter(E,b.VALIDATE_STATUS)+", gl error ["+b.getError()+"]");E.uniforms={};E.attributes={};l.program=E;t=["viewMatrix","modelViewMatrix","projectionMatrix","normalMatrix","objectMatrix","cameraPosition"];for(m in l.uniforms)t.push(m);m=l.program;n=0;for(E=t.length;n<E;n++){A=t[n];m.uniforms[A]=b.getUniformLocation(m,A)}m=l.program;t=["position","normal","uv","tangent"];n=0;for(E=t.length;n<E;n++){A=t[n];m.attributes[A]=b.getAttribLocation(m,A)}}m=l.program;if(m!=i){b.useProgram(m);
|
||||
i=m}this.loadCamera(m,f);this.loadMatrices(m);if(l instanceof THREE.MeshPhongMaterial||l instanceof THREE.MeshLambertMaterial){this.setupLights(m,j);f=this.lights;l.uniforms.enableLighting.value=f.directional.length+f.point.length;l.uniforms.ambientLightColor.value=f.ambient;l.uniforms.directionalLightColor.value=f.directional.colors;l.uniforms.directionalLightDirection.value=f.directional.positions;l.uniforms.pointLightColor.value=f.point.colors;l.uniforms.pointLightPosition.value=f.point.positions}if(l instanceof
|
||||
THREE.MeshBasicMaterial||l instanceof THREE.MeshLambertMaterial||l instanceof THREE.MeshPhongMaterial)d(l,q);l instanceof THREE.LineBasicMaterial&&e(l,q);if(l instanceof THREE.MeshPhongMaterial){l.uniforms.ambient.value.setRGB(l.ambient.r,l.ambient.g,l.ambient.b);l.uniforms.specular.value.setRGB(l.specular.r,l.specular.g,l.specular.b);l.uniforms.shininess.value=l.shininess}q=l.uniforms;for(v in q)if(n=m.uniforms[v]){j=q[v];t=j.type;f=j.value;if(t=="i")b.uniform1i(n,f);else if(t=="f")b.uniform1f(n,
|
||||
f);else if(t=="fv1")b.uniform1fv(n,f);else if(t=="fv")b.uniform3fv(n,f);else if(t=="v2")b.uniform2f(n,f.x,f.y);else if(t=="v3")b.uniform3f(n,f.x,f.y,f.z);else if(t=="c")b.uniform3f(n,f.r,f.g,f.b);else if(t=="t"){b.uniform1i(n,f);if(j=j.texture)if(j.image instanceof Array&&j.image.length==6){j=j;f=f;if(j.image.length==6){if(!j.image.__webGLTextureCube&&!j.image.__cubeMapInitialized&&j.image.loadCount==6){j.image.__webGLTextureCube=b.createTexture();b.bindTexture(b.TEXTURE_CUBE_MAP,j.image.__webGLTextureCube);
|
||||
b.texParameteri(b.TEXTURE_CUBE_MAP,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_CUBE_MAP,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_CUBE_MAP,b.TEXTURE_MAG_FILTER,b.LINEAR);b.texParameteri(b.TEXTURE_CUBE_MAP,b.TEXTURE_MIN_FILTER,b.LINEAR_MIPMAP_LINEAR);for(t=0;t<6;++t)b.texImage2D(b.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,b.RGBA,b.RGBA,b.UNSIGNED_BYTE,j.image[t]);b.generateMipmap(b.TEXTURE_CUBE_MAP);b.bindTexture(b.TEXTURE_CUBE_MAP,null);j.image.__cubeMapInitialized=true}b.activeTexture(b.TEXTURE0+
|
||||
f);b.bindTexture(b.TEXTURE_CUBE_MAP,j.image.__webGLTextureCube)}}else{j=j;f=f;if(!j.__webGLTexture&&j.image.loaded){j.__webGLTexture=b.createTexture();b.bindTexture(b.TEXTURE_2D,j.__webGLTexture);b.texImage2D(b.TEXTURE_2D,0,b.RGBA,b.RGBA,b.UNSIGNED_BYTE,j.image);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,h(j.wrap_s));b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,h(j.wrap_t));b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,h(j.mag_filter));b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,h(j.min_filter));
|
||||
b.generateMipmap(b.TEXTURE_2D);b.bindTexture(b.TEXTURE_2D,null)}b.activeTexture(b.TEXTURE0+f);b.bindTexture(b.TEXTURE_2D,j.__webGLTexture)}}}v=m.attributes;b.bindBuffer(b.ARRAY_BUFFER,r.__webGLVertexBuffer);b.vertexAttribPointer(v.position,3,b.FLOAT,false,0,0);b.enableVertexAttribArray(v.position);if(v.normal>=0){b.bindBuffer(b.ARRAY_BUFFER,r.__webGLNormalBuffer);b.vertexAttribPointer(v.normal,3,b.FLOAT,false,0,0);b.enableVertexAttribArray(v.normal)}if(v.tangent>=0){b.bindBuffer(b.ARRAY_BUFFER,r.__webGLTangentBuffer);
|
||||
b.vertexAttribPointer(v.tangent,4,b.FLOAT,false,0,0);b.enableVertexAttribArray(v.tangent)}if(v.uv>=0)if(r.__webGLUVBuffer){b.bindBuffer(b.ARRAY_BUFFER,r.__webGLUVBuffer);b.vertexAttribPointer(v.uv,2,b.FLOAT,false,0,0);b.enableVertexAttribArray(v.uv)}else b.disableVertexAttribArray(v.uv);if(l.wireframe||l instanceof THREE.LineBasicMaterial){v=l.wireframe_linewidth!==undefined?l.wireframe_linewidth:l.linewidth!==undefined?l.linewidth:1;l=l instanceof THREE.LineBasicMaterial&&C.type==THREE.LineStrip?
|
||||
b.LINE_STRIP:b.LINES;b.lineWidth(v);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,r.__webGLLineBuffer);b.drawElements(l,r.__webGLLineCount,b.UNSIGNED_SHORT,0)}else{b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,r.__webGLFaceBuffer);b.drawElements(b.TRIANGLES,r.__webGLFaceCount,b.UNSIGNED_SHORT,0)}};this.renderPass=function(f,j,q,l,r,C,m){var t,v,s,n,E;s=0;for(n=l.materials.length;s<n;s++){t=l.materials[s];if(t instanceof THREE.MeshFaceMaterial){t=0;for(v=r.materials.length;t<v;t++)if((E=r.materials[t])&&E.blending==C&&
|
||||
E.opacity<1==m){this.setBlending(E.blending);this.renderBuffer(f,j,q,E,r,l)}}else if((E=t)&&E.blending==C&&E.opacity<1==m){this.setBlending(E.blending);this.renderBuffer(f,j,q,E,r,l)}}};this.render=function(f,j,q,l){var r,C,m,t=f.lights,v=f.fog;this.initWebGLObjects(f);l=l!==undefined?l:true;if(q&&!q.__webGLFramebuffer){q.__webGLFramebuffer=b.createFramebuffer();q.__webGLRenderbuffer=b.createRenderbuffer();q.__webGLTexture=b.createTexture();b.bindRenderbuffer(b.RENDERBUFFER,q.__webGLRenderbuffer);
|
||||
b.renderbufferStorage(b.RENDERBUFFER,b.DEPTH_COMPONENT16,q.width,q.height);b.bindTexture(b.TEXTURE_2D,q.__webGLTexture);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,h(q.wrap_s));b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,h(q.wrap_t));b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,h(q.mag_filter));b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,h(q.min_filter));b.texImage2D(b.TEXTURE_2D,0,h(q.format),q.width,q.height,0,h(q.format),h(q.type),null);b.bindFramebuffer(b.FRAMEBUFFER,q.__webGLFramebuffer);
|
||||
b.framebufferTexture2D(b.FRAMEBUFFER,b.COLOR_ATTACHMENT0,b.TEXTURE_2D,q.__webGLTexture,0);b.framebufferRenderbuffer(b.FRAMEBUFFER,b.DEPTH_ATTACHMENT,b.RENDERBUFFER,q.__webGLRenderbuffer);b.bindTexture(b.TEXTURE_2D,null);b.bindRenderbuffer(b.RENDERBUFFER,null);b.bindFramebuffer(b.FRAMEBUFFER,null)}if(q){r=q.__webGLFramebuffer;m=q.width;C=q.height}else{r=null;m=o.width;C=o.height}if(r!=k){b.bindFramebuffer(b.FRAMEBUFFER,r);b.viewport(0,0,m,C);l&&b.clear(b.COLOR_BUFFER_BIT|b.DEPTH_BUFFER_BIT);k=r}this.autoClear&&
|
||||
this.clear();j.autoUpdateMatrix&&j.updateMatrix();u.set(j.matrix.flatten());H.set(j.projectionMatrix.flatten());l=0;for(r=f.__webGLObjects.length;l<r;l++){C=f.__webGLObjects[l];m=C.object;C=C.buffer;if(m.visible){this.setupMatrices(m,j);this.renderPass(j,t,v,m,C,THREE.NormalBlending,false)}}l=0;for(r=f.__webGLObjects.length;l<r;l++){C=f.__webGLObjects[l];m=C.object;C=C.buffer;if(m.visible){this.setupMatrices(m,j);if(m.doubleSided)b.disable(b.CULL_FACE);else{b.enable(b.CULL_FACE);m.flipSided?b.frontFace(b.CW):
|
||||
b.frontFace(b.CCW)}this.renderPass(j,t,v,m,C,THREE.AdditiveBlending,false);this.renderPass(j,t,v,m,C,THREE.SubtractiveBlending,false);this.renderPass(j,t,v,m,C,THREE.AdditiveBlending,true);this.renderPass(j,t,v,m,C,THREE.SubtractiveBlending,true);this.renderPass(j,t,v,m,C,THREE.NormalBlending,true)}}if(q&&q.min_filter!==THREE.NearestFilter&&q.min_filter!==THREE.LinearFilter){b.bindTexture(b.TEXTURE_2D,q.__webGLTexture);b.generateMipmap(b.TEXTURE_2D);b.bindTexture(b.TEXTURE_2D,null)}};this.initWebGLObjects=
|
||||
function(f){function j(s,n,E,A){if(s[n]==undefined){f.__webGLObjects.push({buffer:E,object:A});s[n]=1}}var q,l,r,C,m,t,v;if(!f.__webGLObjects){f.__webGLObjects=[];f.__webGLObjectsMap={}}q=0;for(l=f.objects.length;q<l;q++){r=f.objects[q];m=r.geometry;if(f.__webGLObjectsMap[r.id]==undefined)f.__webGLObjectsMap[r.id]={};v=f.__webGLObjectsMap[r.id];if(r instanceof THREE.Mesh){for(C in m.geometryChunks){t=m.geometryChunks[C];if(!t.__webGLVertexBuffer){this.createMeshBuffers(t);this.initMeshBuffers(t,r);
|
||||
m.__dirtyVertices=true;m.__dirtyElements=true;m.__dirtyUvs=true;m.__dirtyNormals=true;m.__dirtyTangents=true}if(m.__dirtyVertices||m.__dirtyElements||m.__dirtyUvs)this.setMeshBuffers(t,r,b.DYNAMIC_DRAW,m.__dirtyVertices,m.__dirtyElements,m.__dirtyUvs,m.__dirtyNormals,m.__dirtyTangents);j(v,C,t,r)}m.__dirtyVertices=false;m.__dirtyElements=false;m.__dirtyUvs=false;m.__dirtyNormals=false;m.__dirtyTangents=false}else if(r instanceof THREE.Line){if(!m.__webGLVertexBuffer){this.createLineBuffers(m);this.initLineBuffers(m);
|
||||
m.__dirtyVertices=true;m.__dirtyElements=true}m.__dirtyVertices&&this.setLineBuffers(m,b.DYNAMIC_DRAW,m.__dirtyVertices,m.__dirtyElements);j(v,0,m,r);m.__dirtyVertices=false;m.__dirtyElements=false}else if(r instanceof THREE.ParticleSystem){m.__webGLVertexBuffer||this.createParticleBuffers(m);j(v,0,m,r)}}};this.removeObject=function(f,j){var q,l;for(q=f.__webGLObjects.length-1;q>=0;q--){l=f.__webGLObjects[q].object;j==l&&f.__webGLObjects.splice(q,1)}};this.setupMatrices=function(f,j){f.autoUpdateMatrix&&
|
||||
f.updateMatrix();y.multiply(j.matrix,f.matrix);x.set(y.flatten());z=THREE.Matrix4.makeInvert3x3(y).transpose();J.set(z.m);K.set(f.matrix.flatten())};this.loadMatrices=function(f){b.uniformMatrix4fv(f.uniforms.viewMatrix,false,u);b.uniformMatrix4fv(f.uniforms.modelViewMatrix,false,x);b.uniformMatrix4fv(f.uniforms.projectionMatrix,false,H);b.uniformMatrix3fv(f.uniforms.normalMatrix,false,J);b.uniformMatrix4fv(f.uniforms.objectMatrix,false,K)};this.loadCamera=function(f,j){b.uniform3f(f.uniforms.cameraPosition,
|
||||
j.position.x,j.position.y,j.position.z)};this.setBlending=function(f){switch(f){case THREE.AdditiveBlending:b.blendEquation(b.FUNC_ADD);b.blendFunc(b.ONE,b.ONE);break;case THREE.SubtractiveBlending:b.blendFunc(b.DST_COLOR,b.ZERO);break;default:b.blendEquation(b.FUNC_ADD);b.blendFunc(b.ONE,b.ONE_MINUS_SRC_ALPHA)}};this.setFaceCulling=function(f,j){if(f){!j||j=="ccw"?b.frontFace(b.CCW):b.frontFace(b.CW);if(f=="back")b.cullFace(b.BACK);else f=="front"?b.cullFace(b.FRONT):b.cullFace(b.FRONT_AND_BACK);
|
||||
b.enable(b.CULL_FACE)}else b.disable(b.CULL_FACE)};this.supportsVertexTextures=function(){return b.getParameter(b.MAX_VERTEX_TEXTURE_IMAGE_UNITS)>0}};
|
||||
THREE.Snippets={fog_pars_fragment:"#ifdef USE_FOG\nuniform vec3 fogColor;\n#ifdef FOG_EXP2\nuniform float fogDensity;\n#else\nuniform float fogNear;\nuniform float fogFar;\n#endif\n#endif",fog_fragment:"#ifdef USE_FOG\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\n#ifdef FOG_EXP2\nconst float LOG2 = 1.442695;\nfloat fogFactor = exp2( - fogDensity * fogDensity * depth * depth * LOG2 );\nfogFactor = 1.0 - clamp( fogFactor, 0.0, 1.0 );\n#else\nfloat fogFactor = smoothstep( fogNear, fogFar, depth );\n#endif\ngl_FragColor = mix( gl_FragColor, vec4( fogColor, 1.0 ), fogFactor );\n#endif",envmap_pars_fragment:"#ifdef USE_ENVMAP\nvarying vec3 vReflect;\nuniform float reflectivity;\nuniform samplerCube env_map;\nuniform int combine;\n#endif",
|
||||
envmap_fragment:"#ifdef USE_ENVMAP\ncubeColor = textureCube( env_map, vec3( -vReflect.x, vReflect.yz ) );\nif ( combine == 1 ) {\ngl_FragColor = mix( gl_FragColor, cubeColor, reflectivity );\n} else {\ngl_FragColor = gl_FragColor * cubeColor;\n}\n#endif",envmap_pars_vertex:"#ifdef USE_ENVMAP\nvarying vec3 vReflect;\nuniform float refraction_ratio;\nuniform bool useRefract;\n#endif",envmap_vertex:"#ifdef USE_ENVMAP\nvec4 mPosition = objectMatrix * vec4( position, 1.0 );\nvec3 nWorld = mat3( objectMatrix[0].xyz, objectMatrix[1].xyz, objectMatrix[2].xyz ) * normal;\nif ( useRefract ) {\nvReflect = refract( normalize( mPosition.xyz - cameraPosition ), normalize( nWorld.xyz ), refraction_ratio );\n} else {\nvReflect = reflect( normalize( mPosition.xyz - cameraPosition ), normalize( nWorld.xyz ) );\n}\n#endif",
|
||||
map_pars_fragment:"#ifdef USE_MAP\nvarying vec2 vUv;\nuniform sampler2D map;\n#endif",map_pars_vertex:"#ifdef USE_MAP\nvarying vec2 vUv;\n#endif",map_fragment:"#ifdef USE_MAP\nmapColor = texture2D( map, vUv );\n#endif",map_vertex:"#ifdef USE_MAP\nvUv = uv;\n#endif",lights_pars_vertex:"uniform bool enableLighting;\nuniform vec3 ambientLightColor;\n#if MAX_DIR_LIGHTS > 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\n#ifdef PHONG\nvarying vec3 vPointLightVector[ MAX_POINT_LIGHTS ];\n#endif\n#endif",
|
||||
lights_vertex:"if ( !enableLighting ) {\nvLightWeighting = vec3( 1.0 );\n} else {\nvLightWeighting = ambientLightColor;\n#if MAX_DIR_LIGHTS > 0\nfor( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nfloat directionalLightWeighting = max( dot( transformedNormal, normalize( lDirection.xyz ) ), 0.0 );\nvLightWeighting += directionalLightColor[ i ] * directionalLightWeighting;\n}\n#endif\n#if MAX_POINT_LIGHTS > 0\nfor( int i = 0; i < MAX_POINT_LIGHTS; i++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 pointLightVector = normalize( lPosition.xyz - mvPosition.xyz );\nfloat pointLightWeighting = max( dot( transformedNormal, pointLightVector ), 0.0 );\nvLightWeighting += pointLightColor[ i ] * pointLightWeighting;\n#ifdef PHONG\nvPointLightVector[ i ] = pointLightVector;\n#endif\n}\n#endif\n}",
|
||||
lights_pars_fragment:"#if MAX_DIR_LIGHTS > 0\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nvarying vec3 vPointLightVector[ MAX_POINT_LIGHTS ];\n#endif\nvarying vec3 vViewPosition;\nvarying vec3 vNormal;",lights_fragment:"vec3 normal = normalize( vNormal );\nvec3 viewPosition = normalize( vViewPosition );\nvec4 mSpecular = vec4( specular, opacity );\n#if MAX_POINT_LIGHTS > 0\nvec4 pointDiffuse = vec4( 0.0 );\nvec4 pointSpecular = vec4( 0.0 );\nfor( int i = 0; i < MAX_POINT_LIGHTS; i++ ) {\nvec3 pointVector = normalize( vPointLightVector[ i ] );\nvec3 pointHalfVector = normalize( vPointLightVector[ i ] + vViewPosition );\nfloat pointDotNormalHalf = dot( normal, pointHalfVector );\nfloat pointDiffuseWeight = max( dot( normal, pointVector ), 0.0 );\nfloat pointSpecularWeight = 0.0;\nif ( pointDotNormalHalf >= 0.0 )\npointSpecularWeight = pow( pointDotNormalHalf, shininess );\npointDiffuse += mColor * pointDiffuseWeight;\npointSpecular += mSpecular * pointSpecularWeight;\n}\n#endif\n#if MAX_DIR_LIGHTS > 0\nvec4 dirDiffuse = vec4( 0.0 );\nvec4 dirSpecular = vec4( 0.0 );\nfor( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\nvec3 dirHalfVector = normalize( lDirection.xyz + vViewPosition );\nfloat dirDotNormalHalf = dot( normal, dirHalfVector );\nfloat dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );\nfloat dirSpecularWeight = 0.0;\nif ( dirDotNormalHalf >= 0.0 )\ndirSpecularWeight = pow( dirDotNormalHalf, shininess );\ndirDiffuse += mColor * dirDiffuseWeight;\ndirSpecular += mSpecular * dirSpecularWeight;\n}\n#endif\nvec4 totalLight = vec4( ambient, opacity );\n#if MAX_DIR_LIGHTS > 0\ntotalLight += dirDiffuse + dirSpecular;\n#endif\n#if MAX_POINT_LIGHTS > 0\ntotalLight += pointDiffuse + pointSpecular;\n#endif"};
|
||||
THREE.UniformsLib={common:{color:{type:"c",value:new THREE.Color(15658734)},opacity:{type:"f",value:1},map:{type:"t",value:0,texture:null},env_map:{type:"t",value:1,texture:null},useRefract:{type:"i",value:0},reflectivity:{type:"f",value:1},refraction_ratio:{type:"f",value:0.98},combine:{type:"i",value:0},fogDensity:{type:"f",value:2.5E-4},fogNear:{type:"f",value:1},fogFar:{type:"f",value:2E3},fogColor:{type:"c",value:new THREE.Color(16777215)}},lights:{enableLighting:{type:"i",value:1},ambientLightColor:{type:"fv",
|
||||
value:[]},directionalLightDirection:{type:"fv",value:[]},directionalLightColor:{type:"fv",value:[]},pointLightPosition:{type:"fv",value:[]},pointLightColor:{type:"fv",value:[]}}};
|
||||
THREE.ShaderLib={depth:{uniforms:{mNear:{type:"f",value:1},mFar:{type:"f",value:2E3}},fragment_shader:"uniform float mNear;\nuniform float mFar;\nvoid main() {\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\nfloat color = 1.0 - smoothstep( mNear, mFar, depth );\ngl_FragColor = vec4( vec3( color ), 1.0 );\n}",vertex_shader:"void main() {\ngl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}"},normal:{uniforms:{},fragment_shader:"varying vec3 vNormal;\nvoid main() {\ngl_FragColor = vec4( 0.5 * normalize( vNormal ) + 0.5, 1.0 );\n}",
|
||||
vertex_shader:"varying vec3 vNormal;\nvoid main() {\nvec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\nvNormal = normalize( normalMatrix * normal );\ngl_Position = projectionMatrix * mvPosition;\n}"},basic:{uniforms:THREE.UniformsLib.common,fragment_shader:["uniform vec3 color;\nuniform float opacity;",THREE.Snippets.map_pars_fragment,THREE.Snippets.envmap_pars_fragment,THREE.Snippets.fog_pars_fragment,"void main() {\nvec4 mColor = vec4( color, opacity );\nvec4 mapColor = vec4( 1.0 );\nvec4 cubeColor = vec4( 1.0 );",
|
||||
THREE.Snippets.map_fragment,"gl_FragColor = mColor * mapColor;",THREE.Snippets.envmap_fragment,THREE.Snippets.fog_fragment,"}"].join("\n"),vertex_shader:[THREE.Snippets.map_pars_vertex,THREE.Snippets.envmap_pars_vertex,"void main() {\nvec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",THREE.Snippets.map_vertex,THREE.Snippets.envmap_vertex,"gl_Position = projectionMatrix * mvPosition;\n}"].join("\n")},lambert:{uniforms:Uniforms.merge([THREE.UniformsLib.common,THREE.UniformsLib.lights]),fragment_shader:["uniform vec3 color;\nuniform float opacity;\nvarying vec3 vLightWeighting;",
|
||||
THREE.Snippets.map_pars_fragment,THREE.Snippets.envmap_pars_fragment,THREE.Snippets.fog_pars_fragment,"void main() {\nvec4 mColor = vec4( color, opacity );\nvec4 mapColor = vec4( 1.0 );\nvec4 cubeColor = vec4( 1.0 );",THREE.Snippets.map_fragment,"gl_FragColor = mColor * mapColor * vec4( vLightWeighting, 1.0 );",THREE.Snippets.envmap_fragment,THREE.Snippets.fog_fragment,"}"].join("\n"),vertex_shader:["varying vec3 vLightWeighting;",THREE.Snippets.map_pars_vertex,THREE.Snippets.envmap_pars_vertex,
|
||||
THREE.Snippets.lights_pars_vertex,"void main() {\nvec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",THREE.Snippets.map_vertex,THREE.Snippets.envmap_vertex,"vec3 transformedNormal = normalize( normalMatrix * normal );",THREE.Snippets.lights_vertex,"gl_Position = projectionMatrix * mvPosition;\n}"].join("\n")},phong:{uniforms:Uniforms.merge([THREE.UniformsLib.common,THREE.UniformsLib.lights,{ambient:{type:"c",value:new THREE.Color(328965)},specular:{type:"c",value:new THREE.Color(1118481)},
|
||||
shininess:{type:"f",value:30}}]),fragment_shader:["uniform vec3 color;\nuniform float opacity;\nuniform vec3 ambient;\nuniform vec3 specular;\nuniform float shininess;\nvarying vec3 vLightWeighting;",THREE.Snippets.map_pars_fragment,THREE.Snippets.envmap_pars_fragment,THREE.Snippets.fog_pars_fragment,THREE.Snippets.lights_pars_fragment,"void main() {\nvec4 mColor = vec4( color, opacity );\nvec4 mapColor = vec4( 1.0 );\nvec4 cubeColor = vec4( 1.0 );",THREE.Snippets.map_fragment,THREE.Snippets.lights_fragment,
|
||||
"gl_FragColor = mapColor * totalLight * vec4( vLightWeighting, 1.0 );",THREE.Snippets.envmap_fragment,THREE.Snippets.fog_fragment,"}"].join("\n"),vertex_shader:["#define PHONG\nvarying vec3 vLightWeighting;\nvarying vec3 vViewPosition;\nvarying vec3 vNormal;",THREE.Snippets.map_pars_vertex,THREE.Snippets.envmap_pars_vertex,THREE.Snippets.lights_pars_vertex,"void main() {\nvec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",THREE.Snippets.map_vertex,THREE.Snippets.envmap_vertex,"#ifndef USE_ENVMAP\nvec4 mPosition = objectMatrix * vec4( position, 1.0 );\n#endif\nvViewPosition = cameraPosition - mPosition.xyz;\nvec3 transformedNormal = normalize( normalMatrix * normal );\nvNormal = transformedNormal;",
|
||||
THREE.Snippets.lights_vertex,"gl_Position = projectionMatrix * mvPosition;\n}"].join("\n")}};THREE.RenderableObject=function(){this.z=this.object=null};THREE.RenderableFace3=function(){this.z=null;this.v1=new THREE.Vertex;this.v2=new THREE.Vertex;this.v3=new THREE.Vertex;this.centroidWorld=new THREE.Vector3;this.centroidScreen=new THREE.Vector3;this.normalWorld=new THREE.Vector3;this.vertexNormalsWorld=[];this.faceMaterials=this.meshMaterials=null;this.overdraw=false;this.uvs=[null,null,null]};
|
||||
THREE.RenderableParticle=function(){this.rotation=this.z=this.y=this.x=null;this.scale=new THREE.Vector2;this.materials=null};THREE.RenderableLine=function(){this.z=null;this.v1=new THREE.Vertex;this.v2=new THREE.Vertex;this.materials=null};
|
126
extlib/thingiview.js/binaryReader.js
Normal file
126
extlib/thingiview.js/binaryReader.js
Normal file
@ -0,0 +1,126 @@
|
||||
// BinaryReader
|
||||
// Refactored by Vjeux <vjeuxx@gmail.com>
|
||||
// http://blog.vjeux.com/2010/javascript/javascript-binary-reader.html
|
||||
|
||||
// Original
|
||||
//+ Jonas Raoni Soares Silva
|
||||
//@ http://jsfromhell.com/classes/binary-parser [rev. #1]
|
||||
|
||||
BinaryReader = function (data) {
|
||||
this._buffer = data;
|
||||
this._pos = 0;
|
||||
};
|
||||
|
||||
BinaryReader.prototype = {
|
||||
|
||||
/* Public */
|
||||
|
||||
readInt8: function (){ return this._decodeInt(8, true); },
|
||||
readUInt8: function (){ return this._decodeInt(8, false); },
|
||||
readInt16: function (){ return this._decodeInt(16, true); },
|
||||
readUInt16: function (){ return this._decodeInt(16, false); },
|
||||
readInt32: function (){ return this._decodeInt(32, true); },
|
||||
readUInt32: function (){ return this._decodeInt(32, false); },
|
||||
|
||||
readFloat: function (){ return this._decodeFloat(23, 8); },
|
||||
readDouble: function (){ return this._decodeFloat(52, 11); },
|
||||
|
||||
readChar: function () { return this.readString(1); },
|
||||
readString: function (length) {
|
||||
this._checkSize(length * 8);
|
||||
var result = this._buffer.substr(this._pos, length);
|
||||
this._pos += length;
|
||||
return result;
|
||||
},
|
||||
|
||||
seek: function (pos) {
|
||||
this._pos = pos;
|
||||
this._checkSize(0);
|
||||
},
|
||||
|
||||
getPosition: function () {
|
||||
return this._pos;
|
||||
},
|
||||
|
||||
getSize: function () {
|
||||
return this._buffer.length;
|
||||
},
|
||||
|
||||
|
||||
/* Private */
|
||||
|
||||
_decodeFloat: function(precisionBits, exponentBits){
|
||||
var length = precisionBits + exponentBits + 1;
|
||||
var size = length >> 3;
|
||||
this._checkSize(length);
|
||||
|
||||
var bias = Math.pow(2, exponentBits - 1) - 1;
|
||||
var signal = this._readBits(precisionBits + exponentBits, 1, size);
|
||||
var exponent = this._readBits(precisionBits, exponentBits, size);
|
||||
var significand = 0;
|
||||
var divisor = 2;
|
||||
// var curByte = length + (-precisionBits >> 3) - 1;
|
||||
var curByte = 0;
|
||||
do {
|
||||
var byteValue = this._readByte(++curByte, size);
|
||||
var startBit = precisionBits % 8 || 8;
|
||||
var mask = 1 << startBit;
|
||||
while (mask >>= 1) {
|
||||
if (byteValue & mask) {
|
||||
significand += 1 / divisor;
|
||||
}
|
||||
divisor *= 2;
|
||||
}
|
||||
} while (precisionBits -= startBit);
|
||||
|
||||
this._pos += size;
|
||||
|
||||
return exponent == (bias << 1) + 1 ? significand ? NaN : signal ? -Infinity : +Infinity
|
||||
: (1 + signal * -2) * (exponent || significand ? !exponent ? Math.pow(2, -bias + 1) * significand
|
||||
: Math.pow(2, exponent - bias) * (1 + significand) : 0);
|
||||
},
|
||||
|
||||
_decodeInt: function(bits, signed){
|
||||
var x = this._readBits(0, bits, bits / 8), max = Math.pow(2, bits);
|
||||
var result = signed && x >= max / 2 ? x - max : x;
|
||||
|
||||
this._pos += bits / 8;
|
||||
return result;
|
||||
},
|
||||
|
||||
//shl fix: Henri Torgemane ~1996 (compressed by Jonas Raoni)
|
||||
_shl: function (a, b){
|
||||
for (++b; --b; a = ((a %= 0x7fffffff + 1) & 0x40000000) == 0x40000000 ? a * 2 : (a - 0x40000000) * 2 + 0x7fffffff + 1);
|
||||
return a;
|
||||
},
|
||||
|
||||
_readByte: function (i, size) {
|
||||
return this._buffer.charCodeAt(this._pos + size - i - 1) & 0xff;
|
||||
},
|
||||
|
||||
_readBits: function (start, length, size) {
|
||||
var offsetLeft = (start + length) % 8;
|
||||
var offsetRight = start % 8;
|
||||
var curByte = size - (start >> 3) - 1;
|
||||
var lastByte = size + (-(start + length) >> 3);
|
||||
var diff = curByte - lastByte;
|
||||
|
||||
var sum = (this._readByte(curByte, size) >> offsetRight) & ((1 << (diff ? 8 - offsetRight : length)) - 1);
|
||||
|
||||
if (diff && offsetLeft) {
|
||||
sum += (this._readByte(lastByte++, size) & ((1 << offsetLeft) - 1)) << (diff-- << 3) - offsetRight;
|
||||
}
|
||||
|
||||
while (diff) {
|
||||
sum += this._shl(this._readByte(lastByte++, size), (diff-- << 3) - offsetRight);
|
||||
}
|
||||
|
||||
return sum;
|
||||
},
|
||||
|
||||
_checkSize: function (neededBits) {
|
||||
if (!(this._pos + Math.ceil(neededBits / 8) < this._buffer.length)) {
|
||||
throw new Error("Index out of bound");
|
||||
}
|
||||
}
|
||||
};
|
62
extlib/thingiview.js/plane.js
Normal file
62
extlib/thingiview.js/plane.js
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @author mr.doob / http://mrdoob.com/
|
||||
* based on http://papervision3d.googlecode.com/svn/trunk/as3/trunk/src/org/papervision3d/objects/primitives/Plane.as
|
||||
*/
|
||||
|
||||
var Plane = function ( width, height, segments_width, segments_height ) {
|
||||
|
||||
THREE.Geometry.call( this );
|
||||
|
||||
var ix, iy,
|
||||
width_half = width / 2,
|
||||
height_half = height / 2,
|
||||
gridX = segments_width || 1,
|
||||
gridY = segments_height || 1,
|
||||
gridX1 = gridX + 1,
|
||||
gridY1 = gridY + 1,
|
||||
segment_width = width / gridX,
|
||||
segment_height = height / gridY;
|
||||
|
||||
|
||||
for( iy = 0; iy < gridY1; iy++ ) {
|
||||
|
||||
for( ix = 0; ix < gridX1; ix++ ) {
|
||||
|
||||
var x = ix * segment_width - width_half;
|
||||
var y = iy * segment_height - height_half;
|
||||
|
||||
this.vertices.push( new THREE.Vertex( new THREE.Vector3( x, - y, 0 ) ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for( iy = 0; iy < gridY; iy++ ) {
|
||||
|
||||
for( ix = 0; ix < gridX; ix++ ) {
|
||||
|
||||
var a = ix + gridX1 * iy;
|
||||
var b = ix + gridX1 * ( iy + 1 );
|
||||
var c = ( ix + 1 ) + gridX1 * ( iy + 1 );
|
||||
var d = ( ix + 1 ) + gridX1 * iy;
|
||||
|
||||
this.faces.push( new THREE.Face4( a, b, c, d ) );
|
||||
this.uvs.push( [
|
||||
new THREE.UV( ix / gridX, iy / gridY ),
|
||||
new THREE.UV( ix / gridX, ( iy + 1 ) / gridY ),
|
||||
new THREE.UV( ( ix + 1 ) / gridX, ( iy + 1 ) / gridY ),
|
||||
new THREE.UV( ( ix + 1 ) / gridX, iy / gridY )
|
||||
] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.computeCentroids();
|
||||
this.computeFaceNormals();
|
||||
this.sortFacesByMaterial();
|
||||
|
||||
};
|
||||
|
||||
Plane.prototype = new THREE.Geometry();
|
||||
Plane.prototype.constructor = Plane;
|
2
extlib/thingiview.js/stats.js
Normal file
2
extlib/thingiview.js/stats.js
Normal file
@ -0,0 +1,2 @@
|
||||
// stats.js r5 - http://github.com/mrdoob/stats.js
|
||||
var Stats=function(){var j=0,u=2,r,C=0,E=new Date().getTime(),w=E,f=E,m=0,e=1000,i=0,F,q,c,d,B,k=0,G=1000,a=0,A,t,p,D,l,v=0,o=1000,s=0,h,n,z,g,b,y={fps:{bg:{r:16,g:16,b:48},fg:{r:0,g:255,b:255}},ms:{bg:{r:16,g:48,b:16},fg:{r:0,g:255,b:0}},mem:{bg:{r:48,g:16,b:26},fg:{r:255,g:0,b:128}}};r=document.createElement("div");r.style.fontFamily="Helvetica, Arial, sans-serif";r.style.textAlign="left";r.style.fontSize="9px";r.style.opacity="0.9";r.style.width="80px";r.style.cursor="pointer";r.addEventListener("click",H,false);F=document.createElement("div");F.style.backgroundColor="rgb("+Math.floor(y.fps.bg.r/2)+","+Math.floor(y.fps.bg.g/2)+","+Math.floor(y.fps.bg.b/2)+")";F.style.padding="2px 0px 3px 0px";r.appendChild(F);q=document.createElement("div");q.innerHTML="<strong>FPS</strong>";q.style.color="rgb("+y.fps.fg.r+","+y.fps.fg.g+","+y.fps.fg.b+")";q.style.margin="0px 0px 1px 3px";F.appendChild(q);c=document.createElement("canvas");c.width=74;c.height=30;c.style.display="block";c.style.marginLeft="3px";F.appendChild(c);d=c.getContext("2d");d.fillStyle="rgb("+y.fps.bg.r+","+y.fps.bg.g+","+y.fps.bg.b+")";d.fillRect(0,0,c.width,c.height);B=d.getImageData(0,0,c.width,c.height);A=document.createElement("div");A.style.backgroundColor="rgb("+Math.floor(y.ms.bg.r/2)+","+Math.floor(y.ms.bg.g/2)+","+Math.floor(y.ms.bg.b/2)+")";A.style.padding="2px 0px 3px 0px";A.style.display="none";r.appendChild(A);t=document.createElement("div");t.innerHTML="<strong>MS</strong>";t.style.color="rgb("+y.ms.fg.r+","+y.ms.fg.g+","+y.ms.fg.b+")";t.style.margin="0px 0px 1px 3px";A.appendChild(t);p=document.createElement("canvas");p.width=74;p.height=30;p.style.display="block";p.style.marginLeft="3px";A.appendChild(p);D=p.getContext("2d");D.fillStyle="rgb("+y.ms.bg.r+","+y.ms.bg.g+","+y.ms.bg.b+")";D.fillRect(0,0,p.width,p.height);l=D.getImageData(0,0,p.width,p.height);try{if(webkitPerformance&&webkitPerformance.memory.totalJSHeapSize){u=3}}catch(x){}h=document.createElement("div");h.style.backgroundColor="rgb("+Math.floor(y.mem.bg.r/2)+","+Math.floor(y.mem.bg.g/2)+","+Math.floor(y.mem.bg.b/2)+")";h.style.padding="2px 0px 3px 0px";h.style.display="none";r.appendChild(h);n=document.createElement("div");n.innerHTML="<strong>MEM</strong>";n.style.color="rgb("+y.mem.fg.r+","+y.mem.fg.g+","+y.mem.fg.b+")";n.style.margin="0px 0px 1px 3px";h.appendChild(n);z=document.createElement("canvas");z.width=74;z.height=30;z.style.display="block";z.style.marginLeft="3px";h.appendChild(z);g=z.getContext("2d");g.fillStyle="#301010";g.fillRect(0,0,z.width,z.height);b=g.getImageData(0,0,z.width,z.height);function I(N,M,K){var J,O,L;for(O=0;O<30;O++){for(J=0;J<73;J++){L=(J+O*74)*4;N[L]=N[L+4];N[L+1]=N[L+5];N[L+2]=N[L+6]}}for(O=0;O<30;O++){L=(73+O*74)*4;if(O<M){N[L]=y[K].bg.r;N[L+1]=y[K].bg.g;N[L+2]=y[K].bg.b}else{N[L]=y[K].fg.r;N[L+1]=y[K].fg.g;N[L+2]=y[K].fg.b}}}function H(){j++;j==u?j=0:j;F.style.display="none";A.style.display="none";h.style.display="none";switch(j){case 0:F.style.display="block";break;case 1:A.style.display="block";break;case 2:h.style.display="block";break}}return{domElement:r,update:function(){C++;E=new Date().getTime();k=E-w;G=Math.min(G,k);a=Math.max(a,k);I(l.data,Math.min(30,30-(k/200)*30),"ms");t.innerHTML="<strong>"+k+" MS</strong> ("+G+"-"+a+")";D.putImageData(l,0,0);w=E;if(E>f+1000){m=Math.round((C*1000)/(E-f));e=Math.min(e,m);i=Math.max(i,m);I(B.data,Math.min(30,30-(m/100)*30),"fps");q.innerHTML="<strong>"+m+" FPS</strong> ("+e+"-"+i+")";d.putImageData(B,0,0);if(u==3){v=webkitPerformance.memory.usedJSHeapSize*9.54e-7;o=Math.min(o,v);s=Math.max(s,v);I(b.data,Math.min(30,30-(v/2)),"mem");n.innerHTML="<strong>"+Math.round(v)+" MEM</strong> ("+Math.round(o)+"-"+Math.round(s)+")";g.putImageData(b,0,0)}f=E;C=0}}}};
|
318
extlib/thingiview.js/thingiloader.js
Normal file
318
extlib/thingiview.js/thingiloader.js
Normal file
@ -0,0 +1,318 @@
|
||||
Thingiloader = function(event) {
|
||||
// Code from https://developer.mozilla.org/En/Using_XMLHttpRequest#Receiving_binary_data
|
||||
this.load_binary_resource = function(url) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', url, false);
|
||||
// The following line says we want to receive data as Binary and not as Unicode
|
||||
req.overrideMimeType('text/plain; charset=x-user-defined');
|
||||
req.send(null);
|
||||
if (req.status != 200) return '';
|
||||
|
||||
return req.responseText;
|
||||
};
|
||||
|
||||
this.loadSTL = function(url) {
|
||||
var looksLikeBinary = function(reader) {
|
||||
// STL files don't specify a way to distinguish ASCII from binary.
|
||||
// The usual way is checking for "solid" at the start of the file --
|
||||
// but Thingiverse has seen at least one binary STL file in the wild
|
||||
// that breaks this.
|
||||
|
||||
// The approach here is different: binary STL files contain a triangle
|
||||
// count early in the file. If this correctly predicts the file's length,
|
||||
// it is most probably a binary STL file.
|
||||
|
||||
reader.seek(80); // skip the header
|
||||
var count = reader.readUInt32();
|
||||
|
||||
var predictedSize = 80 /* header */ + 4 /* count */ + 50 * count;
|
||||
return reader.getSize() == predictedSize;
|
||||
};
|
||||
|
||||
workerFacadeMessage({'status':'message', 'content':'Downloading ' + url});
|
||||
var file = this.load_binary_resource(url);
|
||||
var reader = new BinaryReader(file);
|
||||
|
||||
if (looksLikeBinary(reader)) {
|
||||
this.loadSTLBinary(reader);
|
||||
} else {
|
||||
this.loadSTLString(file);
|
||||
}
|
||||
};
|
||||
|
||||
this.loadOBJ = function(url) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Downloading ' + url});
|
||||
var file = this.load_binary_resource(url);
|
||||
this.loadOBJString(file);
|
||||
};
|
||||
|
||||
this.loadJSON = function(url) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Downloading ' + url});
|
||||
var file = this.load_binary_resource(url);
|
||||
this.loadJSONString(file);
|
||||
};
|
||||
|
||||
this.loadPLY = function(url) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Downloading ' + url});
|
||||
|
||||
var file = this.load_binary_resource(url);
|
||||
|
||||
if (file.match(/format ascii/i)) {
|
||||
this.loadPLYString(file);
|
||||
} else {
|
||||
this.loadPLYBinary(file);
|
||||
}
|
||||
};
|
||||
|
||||
this.loadSTLString = function(STLString) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing STL String...'});
|
||||
workerFacadeMessage({'status':'complete', 'content':this.ParseSTLString(STLString)});
|
||||
};
|
||||
|
||||
this.loadSTLBinary = function(STLBinary) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing STL Binary...'});
|
||||
workerFacadeMessage({'status':'complete', 'content':this.ParseSTLBinary(STLBinary)});
|
||||
};
|
||||
|
||||
this.loadOBJString = function(OBJString) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing OBJ String...'});
|
||||
workerFacadeMessage({'status':'complete', 'content':this.ParseOBJString(OBJString)});
|
||||
};
|
||||
|
||||
this.loadJSONString = function(JSONString) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing JSON String...'});
|
||||
workerFacadeMessage({'status':'complete', 'content':eval(JSONString)});
|
||||
};
|
||||
|
||||
this.loadPLYString = function(PLYString) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing PLY String...'});
|
||||
workerFacadeMessage({'status':'complete_points', 'content':this.ParsePLYString(PLYString)});
|
||||
};
|
||||
|
||||
this.loadPLYBinary = function(PLYBinary) {
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing PLY Binary...'});
|
||||
workerFacadeMessage({'status':'complete_points', 'content':this.ParsePLYBinary(PLYBinary)});
|
||||
};
|
||||
|
||||
this.ParsePLYString = function(input) {
|
||||
var properties = [];
|
||||
var vertices = [];
|
||||
var colors = [];
|
||||
|
||||
var vertex_count = 0;
|
||||
|
||||
var header = /ply\n([\s\S]+)\nend_header/ig.exec(input)[1];
|
||||
var data = /end_header\n([\s\S]+)$/ig.exec(input)[1];
|
||||
|
||||
// workerFacadeMessage({'status':'message', 'content':'header:\n' + header});
|
||||
// workerFacadeMessage({'status':'message', 'content':'data:\n' + data});
|
||||
|
||||
header_parts = header.split("\n");
|
||||
|
||||
for (i in header_parts) {
|
||||
if (/element vertex/i.test(header_parts[i])) {
|
||||
vertex_count = /element vertex (\d+)/i.exec(header_parts[i])[1];
|
||||
} else if (/property/i.test(header_parts[i])) {
|
||||
properties.push(/property (.*) (.*)/i.exec(header_parts[i])[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// workerFacadeMessage({'status':'message', 'content':'properties: ' + properties});
|
||||
|
||||
data_parts = data.split("\n");
|
||||
|
||||
for (i in data_parts) {
|
||||
data_line = data_parts[i];
|
||||
data_line_parts = data_line.split(" ");
|
||||
|
||||
vertices.push([
|
||||
parseFloat(data_line_parts[properties.indexOf("x")]),
|
||||
parseFloat(data_line_parts[properties.indexOf("y")]),
|
||||
parseFloat(data_line_parts[properties.indexOf("z")])
|
||||
]);
|
||||
|
||||
colors.push([
|
||||
parseInt(data_line_parts[properties.indexOf("red")]),
|
||||
parseInt(data_line_parts[properties.indexOf("green")]),
|
||||
parseInt(data_line_parts[properties.indexOf("blue")])
|
||||
]);
|
||||
}
|
||||
|
||||
// workerFacadeMessage({'status':'message', 'content':'vertices: ' + vertices});
|
||||
|
||||
return [vertices, colors];
|
||||
};
|
||||
|
||||
this.ParsePLYBinary = function(input) {
|
||||
return false;
|
||||
};
|
||||
|
||||
this.ParseSTLBinary = function(input) {
|
||||
// Skip the header.
|
||||
input.seek(80);
|
||||
|
||||
// Load the number of vertices.
|
||||
var count = input.readUInt32();
|
||||
|
||||
// During the parse loop we maintain the following data structures:
|
||||
var vertices = []; // Append-only list of all unique vertices.
|
||||
var vert_hash = {}; // Mapping from vertex to index in 'vertices', above.
|
||||
var faces = []; // List of triangle descriptions, each a three-element
|
||||
// list of indices in 'vertices', above.
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
if (i % 100 == 0) {
|
||||
workerFacadeMessage({
|
||||
'status':'message',
|
||||
'content':'Parsing ' + (i+1) + ' of ' + count + ' polygons...'
|
||||
});
|
||||
workerFacadeMessage({
|
||||
'status':'progress',
|
||||
'content':parseInt(i / count * 100) + '%'
|
||||
});
|
||||
}
|
||||
|
||||
// Skip the normal (3 single-precision floats)
|
||||
input.seek(input.getPosition() + 12);
|
||||
|
||||
var face_indices = [];
|
||||
for (var x = 0; x < 3; x++) {
|
||||
var vertex = [input.readFloat(), input.readFloat(), input.readFloat()];
|
||||
|
||||
var vertexIndex = vert_hash[vertex];
|
||||
if (vertexIndex == null) {
|
||||
vertexIndex = vertices.length;
|
||||
vertices.push(vertex);
|
||||
vert_hash[vertex] = vertexIndex;
|
||||
}
|
||||
|
||||
face_indices.push(vertexIndex);
|
||||
}
|
||||
faces.push(face_indices);
|
||||
|
||||
// Skip the "attribute" field (unused in common models)
|
||||
input.readUInt16();
|
||||
}
|
||||
|
||||
return [vertices, faces];
|
||||
};
|
||||
|
||||
// build stl's vertex and face arrays
|
||||
this.ParseSTLString = function(STLString) {
|
||||
var vertexes = [];
|
||||
var faces = [];
|
||||
|
||||
var face_vertexes = [];
|
||||
var vert_hash = {}
|
||||
|
||||
// console.log(STLString);
|
||||
|
||||
// strip out extraneous stuff
|
||||
STLString = STLString.replace(/\r/, "\n");
|
||||
STLString = STLString.replace(/^solid[^\n]*/, "");
|
||||
STLString = STLString.replace(/\n/g, " ");
|
||||
STLString = STLString.replace(/facet normal /g,"");
|
||||
STLString = STLString.replace(/outer loop/g,"");
|
||||
STLString = STLString.replace(/vertex /g,"");
|
||||
STLString = STLString.replace(/endloop/g,"");
|
||||
STLString = STLString.replace(/endfacet/g,"");
|
||||
STLString = STLString.replace(/endsolid[^\n]*/, "");
|
||||
STLString = STLString.replace(/\s+/g, " ");
|
||||
STLString = STLString.replace(/^\s+/, "");
|
||||
|
||||
// console.log(STLString);
|
||||
|
||||
var facet_count = 0;
|
||||
var block_start = 0;
|
||||
|
||||
var points = STLString.split(" ");
|
||||
|
||||
workerFacadeMessage({'status':'message', 'content':'Parsing vertices...'});
|
||||
for (var i=0; i<points.length/12-1; i++) {
|
||||
if ((i % 100) == 0) {
|
||||
workerFacadeMessage({'status':'progress', 'content':parseInt(i / (points.length/12-1) * 100) + '%'});
|
||||
}
|
||||
|
||||
var face_indices = [];
|
||||
for (var x=0; x<3; x++) {
|
||||
var vertex = [parseFloat(points[block_start+x*3+3]), parseFloat(points[block_start+x*3+4]), parseFloat(points[block_start+x*3+5])];
|
||||
|
||||
var vertexIndex = vert_hash[vertex];
|
||||
if (vertexIndex == null) {
|
||||
vertexIndex = vertexes.length;
|
||||
vertexes.push(vertex);
|
||||
vert_hash[vertex] = vertexIndex;
|
||||
}
|
||||
|
||||
face_indices.push(vertexIndex);
|
||||
}
|
||||
faces.push(face_indices);
|
||||
|
||||
block_start = block_start + 12;
|
||||
}
|
||||
|
||||
return [vertexes, faces];
|
||||
};
|
||||
|
||||
this.ParseOBJString = function(OBJString) {
|
||||
var vertexes = [];
|
||||
var faces = [];
|
||||
|
||||
var lines = OBJString.split("\n");
|
||||
|
||||
// var normal_position = 0;
|
||||
|
||||
for (var i=0; i<lines.length; i++) {
|
||||
workerFacadeMessage({'status':'progress', 'content':parseInt(i / lines.length * 100) + '%'});
|
||||
|
||||
line_parts = lines[i].replace(/\s+/g, " ").split(" ");
|
||||
|
||||
if (line_parts[0] == "v") {
|
||||
vertexes.push([parseFloat(line_parts[1]), parseFloat(line_parts[2]), parseFloat(line_parts[3])]);
|
||||
} else if (line_parts[0] == "f") {
|
||||
faces.push([parseFloat(line_parts[1].split("/")[0])-1, parseFloat(line_parts[2].split("/")[0])-1, parseFloat(line_parts[3].split("/")[0]-1), 0])
|
||||
}
|
||||
}
|
||||
|
||||
return [vertexes, faces];
|
||||
};
|
||||
|
||||
switch(event.data.cmd) {
|
||||
case "loadSTL":
|
||||
this.loadSTL(event.data.param);
|
||||
break;
|
||||
case "loadSTLString":
|
||||
this.loadSTLString(event.data.param);
|
||||
break;
|
||||
case "loadSTLBinary":
|
||||
this.loadSTLBinary(event.data.param);
|
||||
break;
|
||||
case "loadOBJ":
|
||||
this.loadOBJ(event.data.param);
|
||||
break;
|
||||
case "loadOBJString":
|
||||
this.loadOBJString(event.data.param);
|
||||
break;
|
||||
case "loadJSON":
|
||||
this.loadJSON(event.data.param);
|
||||
break;
|
||||
case "loadPLY":
|
||||
this.loadPLY(event.data.param);
|
||||
break;
|
||||
case "loadPLYString":
|
||||
this.loadPLYString(event.data.param);
|
||||
break;
|
||||
case "loadPLYBinary":
|
||||
this.loadPLYBinary(event.data.param);
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (typeof(window) === "undefined") {
|
||||
onmessage = Thingiloader;
|
||||
workerFacadeMessage = postMessage;
|
||||
importScripts('binaryReader.js');
|
||||
} else {
|
||||
workerFacadeMessage = WorkerFacade.add(thingiurlbase + "/thingiloader.js", Thingiloader);
|
||||
}
|
898
extlib/thingiview.js/thingiview.js
Normal file
898
extlib/thingiview.js/thingiview.js
Normal file
@ -0,0 +1,898 @@
|
||||
Thingiview = function(containerId) {
|
||||
scope = this;
|
||||
|
||||
this.containerId = containerId;
|
||||
var container = document.getElementById(containerId);
|
||||
|
||||
// var stats = null;
|
||||
var camera = null;
|
||||
var scene = null;
|
||||
var renderer = null;
|
||||
var object = null;
|
||||
var plane = null;
|
||||
|
||||
var ambientLight = null;
|
||||
var directionalLight = null;
|
||||
var pointLight = null;
|
||||
|
||||
var targetXRotation = 0;
|
||||
var targetXRotationOnMouseDown = 0;
|
||||
var mouseX = 0;
|
||||
var mouseXOnMouseDown = 0;
|
||||
|
||||
var targetYRotation = 0;
|
||||
var targetYRotationOnMouseDown = 0;
|
||||
var mouseY = 0;
|
||||
var mouseYOnMouseDown = 0;
|
||||
|
||||
var mouseDown = false;
|
||||
var mouseOver = false;
|
||||
|
||||
var windowHalfX = window.innerWidth / 2;
|
||||
var windowHalfY = window.innerHeight / 2
|
||||
|
||||
var view = null;
|
||||
var infoMessage = null;
|
||||
var progressBar = null;
|
||||
var alertBox = null;
|
||||
|
||||
var timer = null;
|
||||
|
||||
var rotateTimer = null;
|
||||
var rotateListener = null;
|
||||
var wasRotating = null;
|
||||
|
||||
var cameraView = 'diagonal';
|
||||
var cameraZoom = 0;
|
||||
var rotate = false;
|
||||
var backgroundColor = '#606060';
|
||||
var objectMaterial = 'solid';
|
||||
var objectColor = 0xffffff;
|
||||
var showPlane = true;
|
||||
var isWebGl = false;
|
||||
|
||||
if (document.defaultView && document.defaultView.getComputedStyle) {
|
||||
var width = parseFloat(document.defaultView.getComputedStyle(container,null).getPropertyValue('width'));
|
||||
var height = parseFloat(document.defaultView.getComputedStyle(container,null).getPropertyValue('height'));
|
||||
} else {
|
||||
var width = parseFloat(container.currentStyle.width);
|
||||
var height = parseFloat(container.currentStyle.height);
|
||||
}
|
||||
|
||||
var geometry;
|
||||
|
||||
this.initScene = function() {
|
||||
container.style.position = 'relative';
|
||||
container.innerHTML = '';
|
||||
|
||||
camera = new THREE.Camera(45, width/ height, 1, 100000);
|
||||
camera.updateMatrix();
|
||||
|
||||
scene = new THREE.Scene();
|
||||
|
||||
ambientLight = new THREE.AmbientLight(0x202020);
|
||||
scene.addLight(ambientLight);
|
||||
|
||||
directionalLight = new THREE.DirectionalLight(0xffffff, 0.75);
|
||||
directionalLight.position.x = 1;
|
||||
directionalLight.position.y = 1;
|
||||
directionalLight.position.z = 2;
|
||||
directionalLight.position.normalize();
|
||||
scene.addLight(directionalLight);
|
||||
|
||||
pointLight = new THREE.PointLight(0xffffff, 0.3);
|
||||
pointLight.position.x = 0;
|
||||
pointLight.position.y = -25;
|
||||
pointLight.position.z = 10;
|
||||
scene.addLight(pointLight);
|
||||
|
||||
progressBar = document.createElement('div');
|
||||
progressBar.style.position = 'absolute';
|
||||
progressBar.style.top = '0px';
|
||||
progressBar.style.left = '0px';
|
||||
progressBar.style.backgroundColor = 'red';
|
||||
progressBar.style.padding = '5px';
|
||||
progressBar.style.display = 'none';
|
||||
progressBar.style.overflow = 'visible';
|
||||
progressBar.style.whiteSpace = 'nowrap';
|
||||
progressBar.style.zIndex = 100;
|
||||
container.appendChild(progressBar);
|
||||
|
||||
alertBox = document.createElement('div');
|
||||
alertBox.id = 'alertBox';
|
||||
alertBox.style.position = 'absolute';
|
||||
alertBox.style.top = '25%';
|
||||
alertBox.style.left = '25%';
|
||||
alertBox.style.width = '50%';
|
||||
alertBox.style.height = '50%';
|
||||
alertBox.style.backgroundColor = '#dddddd';
|
||||
alertBox.style.padding = '10px';
|
||||
// alertBox.style.overflowY = 'scroll';
|
||||
alertBox.style.display = 'none';
|
||||
alertBox.style.zIndex = 100;
|
||||
container.appendChild(alertBox);
|
||||
|
||||
// load a blank object
|
||||
// this.loadSTLString('');
|
||||
|
||||
if (showPlane) {
|
||||
loadPlaneGeometry();
|
||||
}
|
||||
|
||||
this.setCameraView(cameraView);
|
||||
this.setObjectMaterial(objectMaterial);
|
||||
|
||||
testCanvas = document.createElement('canvas');
|
||||
try {
|
||||
if (testCanvas.getContext('experimental-webgl')) {
|
||||
// showPlane = false;
|
||||
isWebGl = true;
|
||||
renderer = new THREE.WebGLRenderer();
|
||||
// renderer = new THREE.CanvasRenderer();
|
||||
} else {
|
||||
renderer = new THREE.CanvasRenderer();
|
||||
}
|
||||
} catch(e) {
|
||||
renderer = new THREE.CanvasRenderer();
|
||||
// log("failed webgl detection");
|
||||
}
|
||||
|
||||
// renderer.setSize(container.innerWidth, container.innerHeight);
|
||||
|
||||
renderer.setSize(width, height);
|
||||
renderer.domElement.style.backgroundColor = backgroundColor;
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// stats = new Stats();
|
||||
// stats.domElement.style.position = 'absolute';
|
||||
// stats.domElement.style.top = '0px';
|
||||
// container.appendChild(stats.domElement);
|
||||
|
||||
// TODO: figure out how to get the render window to resize when window resizes
|
||||
// window.addEventListener('resize', onContainerResize(), false);
|
||||
// container.addEventListener('resize', onContainerResize(), false);
|
||||
|
||||
// renderer.domElement.addEventListener('mousemove', onRendererMouseMove, false);
|
||||
window.addEventListener('mousemove', onRendererMouseMove, false);
|
||||
renderer.domElement.addEventListener('mouseover', onRendererMouseOver, false);
|
||||
renderer.domElement.addEventListener('mouseout', onRendererMouseOut, false);
|
||||
renderer.domElement.addEventListener('mousedown', onRendererMouseDown, false);
|
||||
// renderer.domElement.addEventListener('mouseup', onRendererMouseUp, false);
|
||||
window.addEventListener('mouseup', onRendererMouseUp, false);
|
||||
|
||||
renderer.domElement.addEventListener('touchstart', onRendererTouchStart, false);
|
||||
renderer.domElement.addEventListener('touchend', onRendererTouchEnd, false);
|
||||
renderer.domElement.addEventListener('touchmove', onRendererTouchMove, false);
|
||||
|
||||
renderer.domElement.addEventListener('DOMMouseScroll', onRendererScroll, false);
|
||||
renderer.domElement.addEventListener('mousewheel', onRendererScroll, false);
|
||||
renderer.domElement.addEventListener('gesturechange', onRendererGestureChange, false);
|
||||
}
|
||||
|
||||
// FIXME
|
||||
// onContainerResize = function(event) {
|
||||
// width = parseFloat(document.defaultView.getComputedStyle(container,null).getPropertyValue('width'));
|
||||
// height = parseFloat(document.defaultView.getComputedStyle(container,null).getPropertyValue('height'));
|
||||
//
|
||||
// // log("resized width: " + width + ", height: " + height);
|
||||
//
|
||||
// if (renderer) {
|
||||
// renderer.setSize(width, height);
|
||||
// camera.projectionMatrix = THREE.Matrix4.makePerspective(70, width / height, 1, 10000);
|
||||
// sceneLoop();
|
||||
// }
|
||||
// };
|
||||
|
||||
onRendererScroll = function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var rolled = 0;
|
||||
|
||||
if (event.wheelDelta === undefined) {
|
||||
// Firefox
|
||||
// The measurement units of the detail and wheelDelta properties are different.
|
||||
rolled = -40 * event.detail;
|
||||
} else {
|
||||
rolled = event.wheelDelta;
|
||||
}
|
||||
|
||||
if (rolled > 0) {
|
||||
// up
|
||||
scope.setCameraZoom(+10);
|
||||
} else {
|
||||
// down
|
||||
scope.setCameraZoom(-10);
|
||||
}
|
||||
}
|
||||
|
||||
onRendererGestureChange = function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.scale > 1) {
|
||||
scope.setCameraZoom(+5);
|
||||
} else {
|
||||
scope.setCameraZoom(-5);
|
||||
}
|
||||
}
|
||||
|
||||
onRendererMouseOver = function(event) {
|
||||
mouseOver = true;
|
||||
// targetRotation = object.rotation.z;
|
||||
if (timer == null) {
|
||||
// log('starting loop');
|
||||
timer = setInterval(sceneLoop, 1000/60);
|
||||
}
|
||||
}
|
||||
|
||||
onRendererMouseDown = function(event) {
|
||||
// log("down");
|
||||
|
||||
event.preventDefault();
|
||||
mouseDown = true;
|
||||
|
||||
if(scope.getRotation()){
|
||||
wasRotating = true;
|
||||
scope.setRotation(false);
|
||||
} else {
|
||||
wasRotating = false;
|
||||
}
|
||||
|
||||
mouseXOnMouseDown = event.clientX - windowHalfX;
|
||||
mouseYOnMouseDown = event.clientY - windowHalfY;
|
||||
|
||||
targetXRotationOnMouseDown = targetXRotation;
|
||||
targetYRotationOnMouseDown = targetYRotation;
|
||||
}
|
||||
|
||||
onRendererMouseMove = function(event) {
|
||||
// log("move");
|
||||
|
||||
if (mouseDown) {
|
||||
mouseX = event.clientX - windowHalfX;
|
||||
// targetXRotation = targetXRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02;
|
||||
xrot = targetXRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02;
|
||||
|
||||
mouseY = event.clientY - windowHalfY;
|
||||
// targetYRotation = targetYRotationOnMouseDown + (mouseY - mouseYOnMouseDown) * 0.02;
|
||||
yrot = targetYRotationOnMouseDown + (mouseY - mouseYOnMouseDown) * 0.02;
|
||||
|
||||
targetXRotation = xrot;
|
||||
targetYRotation = yrot;
|
||||
}
|
||||
}
|
||||
|
||||
onRendererMouseUp = function(event) {
|
||||
// log("up");
|
||||
if (mouseDown) {
|
||||
mouseDown = false;
|
||||
if (!mouseOver) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (wasRotating) {
|
||||
scope.setRotation(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRendererMouseOut = function(event) {
|
||||
if (!mouseDown) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
mouseOver = false;
|
||||
}
|
||||
|
||||
onRendererTouchStart = function(event) {
|
||||
targetXRotation = object.rotation.z;
|
||||
targetYRotation = object.rotation.x;
|
||||
|
||||
timer = setInterval(sceneLoop, 1000/60);
|
||||
|
||||
if (event.touches.length == 1) {
|
||||
event.preventDefault();
|
||||
|
||||
mouseXOnMouseDown = event.touches[0].pageX - windowHalfX;
|
||||
targetXRotationOnMouseDown = targetXRotation;
|
||||
|
||||
mouseYOnMouseDown = event.touches[0].pageY - windowHalfY;
|
||||
targetYRotationOnMouseDown = targetYRotation;
|
||||
}
|
||||
}
|
||||
|
||||
onRendererTouchEnd = function(event) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
// targetXRotation = object.rotation.z;
|
||||
// targetYRotation = object.rotation.x;
|
||||
}
|
||||
|
||||
onRendererTouchMove = function(event) {
|
||||
if (event.touches.length == 1) {
|
||||
event.preventDefault();
|
||||
|
||||
mouseX = event.touches[0].pageX - windowHalfX;
|
||||
targetXRotation = targetXRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.05;
|
||||
|
||||
mouseY = event.touches[0].pageY - windowHalfY;
|
||||
targetYRotation = targetYRotationOnMouseDown + (mouseY - mouseYOnMouseDown) * 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
sceneLoop = function() {
|
||||
if (object) {
|
||||
// if (view == 'bottom') {
|
||||
// if (showPlane) {
|
||||
// plane.rotation.z = object.rotation.z -= (targetRotation + object.rotation.z) * 0.05;
|
||||
// } else {
|
||||
// object.rotation.z -= (targetRotation + object.rotation.z) * 0.05;
|
||||
// }
|
||||
// } else {
|
||||
// if (showPlane) {
|
||||
// plane.rotation.z = object.rotation.z += (targetRotation - object.rotation.z) * 0.05;
|
||||
// } else {
|
||||
// object.rotation.z += (targetRotation - object.rotation.z) * 0.05;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (showPlane) {
|
||||
plane.rotation.z = object.rotation.z = (targetXRotation - object.rotation.z) * 0.2;
|
||||
plane.rotation.x = object.rotation.x = (targetYRotation - object.rotation.x) * 0.2;
|
||||
} else {
|
||||
object.rotation.z = (targetXRotation - object.rotation.z) * 0.2;
|
||||
object.rotation.x = (targetYRotation - object.rotation.x) * 0.2;
|
||||
}
|
||||
|
||||
// log(object.rotation.x);
|
||||
|
||||
camera.updateMatrix();
|
||||
object.updateMatrix();
|
||||
|
||||
if (showPlane) {
|
||||
plane.updateMatrix();
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
// stats.update();
|
||||
}
|
||||
}
|
||||
|
||||
rotateLoop = function() {
|
||||
// targetRotation += 0.01;
|
||||
targetXRotation += 0.05;
|
||||
sceneLoop();
|
||||
}
|
||||
|
||||
this.getShowPlane = function(){
|
||||
return showPlane;
|
||||
}
|
||||
|
||||
this.setShowPlane = function(show) {
|
||||
showPlane = show;
|
||||
|
||||
if (show) {
|
||||
if (scene && !plane) {
|
||||
loadPlaneGeometry();
|
||||
}
|
||||
plane.material[0].opacity = 1;
|
||||
// plane.updateMatrix();
|
||||
} else {
|
||||
if (scene && plane) {
|
||||
// alert(plane.material[0].opacity);
|
||||
plane.material[0].opacity = 0;
|
||||
// plane.updateMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
sceneLoop();
|
||||
}
|
||||
|
||||
this.getRotation = function() {
|
||||
return rotateTimer !== null;
|
||||
}
|
||||
|
||||
this.resetRotation = function () {
|
||||
if (rotate) {
|
||||
this.setRotation(false);
|
||||
this.setRotation(true);
|
||||
}
|
||||
}
|
||||
|
||||
this.setRotation = function(rotate) {
|
||||
rotation = rotate;
|
||||
|
||||
if (rotate) {
|
||||
rotateTimer = setInterval(rotateLoop, 1000/60);
|
||||
} else {
|
||||
clearInterval(rotateTimer);
|
||||
rotateTimer = null;
|
||||
}
|
||||
|
||||
scope.onSetRotation();
|
||||
}
|
||||
|
||||
this.onSetRotation = function(callback) {
|
||||
if(callback === undefined){
|
||||
if(rotateListener !== null){
|
||||
try{
|
||||
rotateListener(scope.getRotation());
|
||||
} catch(ignored) {}
|
||||
}
|
||||
} else {
|
||||
rotateListener = callback;
|
||||
}
|
||||
}
|
||||
|
||||
this.setCameraView = function(dir) {
|
||||
cameraView = dir;
|
||||
|
||||
targetXRotation = 0;
|
||||
targetYRotation = 0;
|
||||
|
||||
if (object) {
|
||||
object.rotation.x = 0;
|
||||
object.rotation.y = 0;
|
||||
object.rotation.z = 0;
|
||||
}
|
||||
|
||||
if (showPlane && object) {
|
||||
plane.rotation.x = object.rotation.x;
|
||||
plane.rotation.y = object.rotation.y;
|
||||
plane.rotation.z = object.rotation.z;
|
||||
}
|
||||
|
||||
if (dir == 'top') {
|
||||
// camera.position.y = 0;
|
||||
// camera.position.z = 100;
|
||||
// camera.target.position.z = 0;
|
||||
if (showPlane) {
|
||||
plane.flipSided = false;
|
||||
}
|
||||
} else if (dir == 'side') {
|
||||
// camera.position.y = -70;
|
||||
// camera.position.z = 70;
|
||||
// camera.target.position.z = 0;
|
||||
targetYRotation = -4.5;
|
||||
if (showPlane) {
|
||||
plane.flipSided = false;
|
||||
}
|
||||
} else if (dir == 'bottom') {
|
||||
// camera.position.y = 0;
|
||||
// camera.position.z = -100;
|
||||
// camera.target.position.z = 0;
|
||||
if (showPlane) {
|
||||
plane.flipSided = true;
|
||||
}
|
||||
} else {
|
||||
// camera.position.y = -70;
|
||||
// camera.position.z = 70;
|
||||
// camera.target.position.z = 0;
|
||||
if (showPlane) {
|
||||
plane.flipSided = false;
|
||||
}
|
||||
}
|
||||
|
||||
mouseX = targetXRotation;
|
||||
mouseXOnMouseDown = targetXRotation;
|
||||
|
||||
mouseY = targetYRotation;
|
||||
mouseYOnMouseDown = targetYRotation;
|
||||
|
||||
scope.centerCamera();
|
||||
|
||||
sceneLoop();
|
||||
}
|
||||
|
||||
this.setCameraZoom = function(factor) {
|
||||
cameraZoom = factor;
|
||||
|
||||
if (cameraView == 'bottom') {
|
||||
if (camera.position.z + factor > 0) {
|
||||
factor = 0;
|
||||
}
|
||||
} else {
|
||||
if (camera.position.z - factor < 0) {
|
||||
factor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (cameraView == 'top') {
|
||||
camera.position.z -= factor;
|
||||
} else if (cameraView == 'bottom') {
|
||||
camera.position.z += factor;
|
||||
} else if (cameraView == 'side') {
|
||||
camera.position.y += factor;
|
||||
camera.position.z -= factor;
|
||||
} else {
|
||||
camera.position.y += factor;
|
||||
camera.position.z -= factor;
|
||||
}
|
||||
|
||||
sceneLoop();
|
||||
}
|
||||
|
||||
this.getObjectMaterial = function() {
|
||||
return objectMaterial;
|
||||
}
|
||||
|
||||
this.setObjectMaterial = function(type) {
|
||||
objectMaterial = type;
|
||||
|
||||
loadObjectGeometry();
|
||||
}
|
||||
|
||||
this.setBackgroundColor = function(color) {
|
||||
backgroundColor = color
|
||||
|
||||
if (renderer) {
|
||||
renderer.domElement.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
this.setObjectColor = function(color) {
|
||||
objectColor = parseInt(color.replace(/\#/g, ''), 16);
|
||||
|
||||
loadObjectGeometry();
|
||||
}
|
||||
|
||||
this.loadSTL = function(url) {
|
||||
scope.newWorker('loadSTL', url);
|
||||
}
|
||||
|
||||
this.loadOBJ = function(url) {
|
||||
scope.newWorker('loadOBJ', url);
|
||||
}
|
||||
|
||||
this.loadSTLString = function(STLString) {
|
||||
scope.newWorker('loadSTLString', STLString);
|
||||
}
|
||||
|
||||
this.loadSTLBinary = function(STLBinary) {
|
||||
scope.newWorker('loadSTLBinary', STLBinary);
|
||||
}
|
||||
|
||||
this.loadOBJString = function(OBJString) {
|
||||
scope.newWorker('loadOBJString', OBJString);
|
||||
}
|
||||
|
||||
this.loadJSON = function(url) {
|
||||
scope.newWorker('loadJSON', url);
|
||||
}
|
||||
|
||||
this.loadPLY = function(url) {
|
||||
scope.newWorker('loadPLY', url);
|
||||
}
|
||||
|
||||
this.loadPLYString = function(PLYString) {
|
||||
scope.newWorker('loadPLYString', PLYString);
|
||||
}
|
||||
|
||||
this.loadPLYBinary = function(PLYBinary) {
|
||||
scope.newWorker('loadPLYBinary', PLYBinary);
|
||||
}
|
||||
|
||||
this.centerCamera = function() {
|
||||
if (geometry) {
|
||||
// Using method from http://msdn.microsoft.com/en-us/library/bb197900(v=xnagamestudio.10).aspx
|
||||
// log("bounding sphere radius = " + geometry.boundingSphere.radius);
|
||||
|
||||
// look at the center of the object
|
||||
camera.target.position.x = geometry.center_x;
|
||||
camera.target.position.y = geometry.center_y;
|
||||
camera.target.position.z = geometry.center_z;
|
||||
|
||||
// set camera position to center of sphere
|
||||
camera.position.x = geometry.center_x;
|
||||
camera.position.y = geometry.center_y;
|
||||
camera.position.z = geometry.center_z;
|
||||
|
||||
// find distance to center
|
||||
distance = geometry.boundingSphere.radius / Math.sin((camera.fov/2) * (Math.PI / 180));
|
||||
|
||||
// zoom backwards about half that distance, I don't think I'm doing the math or backwards vector calculation correctly?
|
||||
// scope.setCameraZoom(-distance/1.8);
|
||||
// scope.setCameraZoom(-distance/1.5);
|
||||
scope.setCameraZoom(-distance/1.9);
|
||||
|
||||
directionalLight.position.x = geometry.min_y * 2;
|
||||
directionalLight.position.y = geometry.min_y * 2;
|
||||
directionalLight.position.z = geometry.max_z * 2;
|
||||
|
||||
pointLight.position.x = geometry.center_y;
|
||||
pointLight.position.y = geometry.center_y;
|
||||
pointLight.position.z = geometry.max_z * 2;
|
||||
} else {
|
||||
// set to any valid position so it doesn't fail before geometry is available
|
||||
camera.position.y = -70;
|
||||
camera.position.z = 70;
|
||||
camera.target.position.z = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadArray = function(array) {
|
||||
log("loading array...");
|
||||
geometry = new STLGeometry(array);
|
||||
loadObjectGeometry();
|
||||
scope.resetRotation();
|
||||
scope.centerCamera();
|
||||
log("finished loading " + geometry.faces.length + " faces.");
|
||||
}
|
||||
|
||||
this.newWorker = function(cmd, param) {
|
||||
scope.setRotation(false);
|
||||
|
||||
var worker = new WorkerFacade(thingiurlbase + '/thingiloader.js');
|
||||
|
||||
worker.onmessage = function(event) {
|
||||
if (event.data.status == "complete") {
|
||||
progressBar.innerHTML = 'Initializing geometry...';
|
||||
// scene.removeObject(object);
|
||||
geometry = new STLGeometry(event.data.content);
|
||||
loadObjectGeometry();
|
||||
progressBar.innerHTML = '';
|
||||
progressBar.style.display = 'none';
|
||||
|
||||
scope.resetRotation();
|
||||
log("finished loading " + geometry.faces.length + " faces.");
|
||||
scope.centerCamera();
|
||||
} else if (event.data.status == "complete_points") {
|
||||
progressBar.innerHTML = 'Initializing points...';
|
||||
|
||||
geometry = new THREE.Geometry();
|
||||
|
||||
var material = new THREE.ParticleBasicMaterial( { color: 0xff0000, opacity: 1 } );
|
||||
|
||||
// material = new THREE.ParticleBasicMaterial( { size: 35, sizeAttenuation: false} );
|
||||
// material.color.setHSV( 1.0, 0.2, 0.8 );
|
||||
|
||||
for (i in event.data.content[0]) {
|
||||
// for (var i=0; i<10; i++) {
|
||||
vector = new THREE.Vector3( event.data.content[0][i][0], event.data.content[0][i][1], event.data.content[0][i][2] );
|
||||
geometry.vertices.push( new THREE.Vertex( vector ) );
|
||||
}
|
||||
|
||||
particles = new THREE.ParticleSystem( geometry, material );
|
||||
particles.sortParticles = true;
|
||||
particles.updateMatrix();
|
||||
scene.addObject( particles );
|
||||
|
||||
camera.updateMatrix();
|
||||
renderer.render(scene, camera);
|
||||
|
||||
progressBar.innerHTML = '';
|
||||
progressBar.style.display = 'none';
|
||||
|
||||
scope.resetRotation();
|
||||
log("finished loading " + event.data.content[0].length + " points.");
|
||||
// scope.centerCamera();
|
||||
} else if (event.data.status == "progress") {
|
||||
progressBar.style.display = 'block';
|
||||
progressBar.style.width = event.data.content;
|
||||
// log(event.data.content);
|
||||
} else if (event.data.status == "message") {
|
||||
progressBar.style.display = 'block';
|
||||
progressBar.innerHTML = event.data.content;
|
||||
log(event.data.content);
|
||||
} else if (event.data.status == "alert") {
|
||||
scope.displayAlert(event.data.content);
|
||||
} else {
|
||||
alert('Error: ' + event.data);
|
||||
log('Unknown Worker Message: ' + event.data);
|
||||
}
|
||||
}
|
||||
|
||||
worker.onerror = function(error) {
|
||||
log(error);
|
||||
error.preventDefault();
|
||||
}
|
||||
|
||||
worker.postMessage({'cmd':cmd, 'param':param});
|
||||
}
|
||||
|
||||
this.displayAlert = function(msg) {
|
||||
msg = msg + "<br/><br/><center><input type=\"button\" value=\"Ok\" onclick=\"document.getElementById('alertBox').style.display='none'\"></center>"
|
||||
|
||||
alertBox.innerHTML = msg;
|
||||
alertBox.style.display = 'block';
|
||||
|
||||
// log(msg);
|
||||
}
|
||||
|
||||
function loadPlaneGeometry() {
|
||||
// TODO: switch to lines instead of the Plane object so we can get rid of the horizontal lines in canvas renderer...
|
||||
plane = new THREE.Mesh(new Plane(100, 100, 10, 10), new THREE.MeshBasicMaterial({color:0xafafaf,wireframe:true}));
|
||||
scene.addObject(plane);
|
||||
}
|
||||
|
||||
function loadObjectGeometry() {
|
||||
if (scene && geometry) {
|
||||
if (objectMaterial == 'wireframe') {
|
||||
// material = new THREE.MeshColorStrokeMaterial(objectColor, 1, 1);
|
||||
material = new THREE.MeshBasicMaterial({color:objectColor,wireframe:true});
|
||||
} else {
|
||||
if (isWebGl) {
|
||||
// material = new THREE.MeshPhongMaterial(objectColor, objectColor, 0xffffff, 50, 1.0);
|
||||
// material = new THREE.MeshColorFillMaterial(objectColor);
|
||||
// material = new THREE.MeshLambertMaterial({color:objectColor});
|
||||
material = new THREE.MeshLambertMaterial({color:objectColor, shading: THREE.FlatShading});
|
||||
} else {
|
||||
// material = new THREE.MeshColorFillMaterial(objectColor);
|
||||
material = new THREE.MeshLambertMaterial({color:objectColor, shading: THREE.FlatShading});
|
||||
}
|
||||
}
|
||||
|
||||
// scene.removeObject(object);
|
||||
|
||||
if (object) {
|
||||
// shouldn't be needed, but this fixes a bug with webgl not removing previous object when loading a new one dynamically
|
||||
object.materials = [new THREE.MeshBasicMaterial({color:0xffffff, opacity:0})];
|
||||
scene.removeObject(object);
|
||||
// object.geometry = geometry;
|
||||
// object.materials = [material];
|
||||
}
|
||||
|
||||
object = new THREE.Mesh(geometry, material);
|
||||
scene.addObject(object);
|
||||
|
||||
if (objectMaterial != 'wireframe') {
|
||||
object.overdraw = true;
|
||||
object.doubleSided = true;
|
||||
}
|
||||
|
||||
object.updateMatrix();
|
||||
|
||||
targetXRotation = 0;
|
||||
targetYRotation = 0;
|
||||
|
||||
sceneLoop();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var STLGeometry = function(stlArray) {
|
||||
// log("building geometry...");
|
||||
THREE.Geometry.call(this);
|
||||
|
||||
var scope = this;
|
||||
|
||||
// var vertexes = stlArray[0];
|
||||
// var normals = stlArray[1];
|
||||
// var faces = stlArray[2];
|
||||
|
||||
for (var i=0; i<stlArray[0].length; i++) {
|
||||
v(stlArray[0][i][0], stlArray[0][i][1], stlArray[0][i][2]);
|
||||
}
|
||||
|
||||
for (var i=0; i<stlArray[1].length; i++) {
|
||||
f3(stlArray[1][i][0], stlArray[1][i][1], stlArray[1][i][2]);
|
||||
}
|
||||
|
||||
function v(x, y, z) {
|
||||
// log("adding vertex: " + x + "," + y + "," + z);
|
||||
scope.vertices.push( new THREE.Vertex( new THREE.Vector3( x, y, z ) ) );
|
||||
}
|
||||
|
||||
function f3(a, b, c) {
|
||||
// log("adding face: " + a + "," + b + "," + c)
|
||||
scope.faces.push( new THREE.Face3( a, b, c ) );
|
||||
}
|
||||
|
||||
// log("computing centroids...");
|
||||
this.computeCentroids();
|
||||
// log("computing normals...");
|
||||
// this.computeNormals();
|
||||
this.computeFaceNormals();
|
||||
this.sortFacesByMaterial();
|
||||
// log("finished building geometry");
|
||||
|
||||
scope.min_x = 0;
|
||||
scope.min_y = 0;
|
||||
scope.min_z = 0;
|
||||
|
||||
scope.max_x = 0;
|
||||
scope.max_y = 0;
|
||||
scope.max_z = 0;
|
||||
|
||||
for (var v = 0, vl = scope.vertices.length; v < vl; v ++) {
|
||||
scope.max_x = Math.max(scope.max_x, scope.vertices[v].position.x);
|
||||
scope.max_y = Math.max(scope.max_y, scope.vertices[v].position.y);
|
||||
scope.max_z = Math.max(scope.max_z, scope.vertices[v].position.z);
|
||||
|
||||
scope.min_x = Math.min(scope.min_x, scope.vertices[v].position.x);
|
||||
scope.min_y = Math.min(scope.min_y, scope.vertices[v].position.y);
|
||||
scope.min_z = Math.min(scope.min_z, scope.vertices[v].position.z);
|
||||
}
|
||||
|
||||
scope.center_x = (scope.max_x + scope.min_x)/2;
|
||||
scope.center_y = (scope.max_y + scope.min_y)/2;
|
||||
scope.center_z = (scope.max_z + scope.min_z)/2;
|
||||
}
|
||||
|
||||
STLGeometry.prototype = new THREE.Geometry();
|
||||
STLGeometry.prototype.constructor = STLGeometry;
|
||||
|
||||
function log(msg) {
|
||||
if (this.console) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* A facade for the Web Worker API that fakes it in case it's missing.
|
||||
Good when web workers aren't supported in the browser, but it's still fast enough, so execution doesn't hang too badly (e.g. Opera 10.5).
|
||||
By Stefan Wehrmeyer, licensed under MIT
|
||||
*/
|
||||
|
||||
var WorkerFacade;
|
||||
if(!!window.Worker){
|
||||
WorkerFacade = (function(){
|
||||
return function(path){
|
||||
return new window.Worker(path);
|
||||
};
|
||||
}());
|
||||
} else {
|
||||
WorkerFacade = (function(){
|
||||
var workers = {}, masters = {}, loaded = false;
|
||||
var that = function(path){
|
||||
var theworker = {}, loaded = false, callings = [];
|
||||
theworker.postToWorkerFunction = function(args){
|
||||
try{
|
||||
workers[path]({"data":args});
|
||||
}catch(err){
|
||||
theworker.onerror(err);
|
||||
}
|
||||
};
|
||||
theworker.postMessage = function(params){
|
||||
if(!loaded){
|
||||
callings.push(params);
|
||||
return;
|
||||
}
|
||||
theworker.postToWorkerFunction(params);
|
||||
};
|
||||
masters[path] = theworker;
|
||||
var scr = document.createElement("SCRIPT");
|
||||
scr.src = path;
|
||||
scr.type = "text/javascript";
|
||||
scr.onload = function(){
|
||||
loaded = true;
|
||||
while(callings.length > 0){
|
||||
theworker.postToWorkerFunction(callings[0]);
|
||||
callings.shift();
|
||||
}
|
||||
};
|
||||
document.body.appendChild(scr);
|
||||
|
||||
var binaryscr = document.createElement("SCRIPT");
|
||||
binaryscr.src = thingiurlbase + '/binaryReader.js';
|
||||
binaryscr.type = "text/javascript";
|
||||
document.body.appendChild(binaryscr);
|
||||
|
||||
return theworker;
|
||||
};
|
||||
that.fake = true;
|
||||
that.add = function(pth, worker){
|
||||
workers[pth] = worker;
|
||||
return function(param){
|
||||
masters[pth].onmessage({"data": param});
|
||||
};
|
||||
};
|
||||
that.toString = function(){
|
||||
return "FakeWorker('"+path+"')";
|
||||
};
|
||||
return that;
|
||||
}());
|
||||
}
|
||||
|
||||
/* Then just use WorkerFacade instead of Worker (or alias it)
|
||||
|
||||
The Worker code must should use a custom function (name it how you want) instead of postMessage.
|
||||
Put this at the end of the Worker:
|
||||
|
||||
if(typeof(window) === "undefined"){
|
||||
onmessage = nameOfWorkerFunction;
|
||||
customPostMessage = postMessage;
|
||||
} else {
|
||||
customPostMessage = WorkerFacade.add("path/to/thisworker.js", nameOfWorkerFunction);
|
||||
}
|
||||
|
||||
*/
|
165
extlib/video-js/LGPLv3-LICENSE.txt
Normal file
165
extlib/video-js/LGPLv3-LICENSE.txt
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Video.js | HTML5 Video Player</title>
|
||||
|
||||
<link href="http://vjs.zencdn.net/c/video-js.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- video.js must be in the <head> for older IEs to work. -->
|
||||
<script src="http://vjs.zencdn.net/c/video.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<video id="example_video_1" class="video-js vjs-default-skin" controls preload="none" width="640" height="264"
|
||||
poster="http://video-js.zencoder.com/oceans-clip.png"
|
||||
data-setup="{}">
|
||||
<source src="http://video-js.zencoder.com/oceans-clip.mp4" type='video/mp4' />
|
||||
<source src="http://video-js.zencoder.com/oceans-clip.webm" type='video/webm' />
|
||||
<source src="http://video-js.zencoder.com/oceans-clip.ogv" type='video/ogg' />
|
||||
</video>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,427 +0,0 @@
|
||||
/*
|
||||
VideoJS Default Styles (http://videojs.com)
|
||||
Version 3.1.0
|
||||
*/
|
||||
|
||||
/*
|
||||
REQUIRED STYLES (be careful overriding)
|
||||
================================================================================ */
|
||||
/* When loading the player, the video tag is replaced with a DIV,
|
||||
that will hold the video tag or object tag for other playback methods.
|
||||
The div contains the video playback element (Flash or HTML5) and controls, and sets the width and height of the video.
|
||||
|
||||
** If you want to add some kind of border/padding (e.g. a frame), or special positioning, use another containing element.
|
||||
Otherwise you risk messing up control positioning and full window mode. **
|
||||
*/
|
||||
.video-js {
|
||||
background-color: #000; position: relative; padding: 0;
|
||||
|
||||
/* Start with 10px for base font size so other dimensions can be em based and easily calculable. */
|
||||
font-size: 10px;
|
||||
|
||||
/* Allow poster to be vertially aligned. */
|
||||
vertical-align: middle;
|
||||
/* display: table-cell; */ /*This works in Safari but not Firefox.*/
|
||||
}
|
||||
|
||||
/* Playback technology elements expand to the width/height of the containing div. <video> or <object> */
|
||||
.video-js .vjs-tech { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||
|
||||
/* Fix for Firefox 9 fullscreen (only if it is enabled). Not needed when checking fullScreenEnabled. */
|
||||
.video-js:-moz-full-screen { position: absolute; }
|
||||
|
||||
/* Fullscreen Styles */
|
||||
body.vjs-full-window {
|
||||
padding: 0; margin: 0;
|
||||
height: 100%; overflow-y: auto; /* Fix for IE6 full-window. http://www.cssplay.co.uk/layouts/fixed.html */
|
||||
}
|
||||
.video-js.vjs-fullscreen {
|
||||
position: fixed; overflow: hidden; z-index: 1000; left: 0; top: 0; bottom: 0; right: 0; width: 100% !important; height: 100% !important;
|
||||
_position: absolute; /* IE6 Full-window (underscore hack) */
|
||||
}
|
||||
.video-js:-webkit-full-screen {
|
||||
width: 100% !important; height: 100% !important;
|
||||
}
|
||||
|
||||
/* Poster Styles */
|
||||
.vjs-poster {
|
||||
margin: 0 auto; padding: 0; cursor: pointer;
|
||||
|
||||
/* Scale with the size of the player div. Works when poster is vertically shorter, but stretches when it's less wide. */
|
||||
position: relative; width: 100%; max-height: 100%;
|
||||
}
|
||||
|
||||
/* Subtiles Styles */
|
||||
.video-js .vjs-subtitles { color: #fff; font-size: 20px; text-align: center; position: absolute; bottom: 40px; left: 0; right: 0; }
|
||||
|
||||
/* Fading sytles, used to fade control bar. */
|
||||
.vjs-fade-in {
|
||||
visibility: visible !important; /* Needed to make sure things hide in older browsers too. */
|
||||
opacity: 1 !important;
|
||||
|
||||
-webkit-transition: visibility 0s linear 0s, opacity 0.3s linear;
|
||||
-moz-transition: visibility 0s linear 0s, opacity 0.3s linear;
|
||||
-ms-transition: visibility 0s linear 0s, opacity 0.3s linear;
|
||||
-o-transition: visibility 0s linear 0s, opacity 0.3s linear;
|
||||
transition: visibility 0s linear 0s, opacity 0.3s linear;
|
||||
}
|
||||
.vjs-fade-out {
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
|
||||
-webkit-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
|
||||
-moz-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
|
||||
-ms-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
|
||||
-o-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
|
||||
transition: visibility 0s linear 1.5s,opacity 1.5s linear;
|
||||
}
|
||||
|
||||
/* DEFAULT SKIN (override in another file to create new skins)
|
||||
================================================================================
|
||||
Instead of editing this file, I recommend creating your own skin CSS file to be included after this file,
|
||||
so you can upgrade to newer versions easier. You can remove all these styles by removing the 'vjs-default-skin' class from the tag. */
|
||||
|
||||
/* The default control bar. Created by bar.js */
|
||||
.vjs-default-skin .vjs-controls {
|
||||
position: absolute;
|
||||
bottom: 0; /* Distance from the bottom of the box/video. Keep 0. Use height to add more bottom margin. */
|
||||
left: 0; right: 0; /* 100% width of div */
|
||||
margin: 0; padding: 0; /* Controls are absolutely position, so no padding necessary */
|
||||
height: 2.6em; /* Including any margin you want above or below control items */
|
||||
color: #fff; border-top: 1px solid #404040;
|
||||
|
||||
/* CSS Gradient */
|
||||
/* Can use the Ultimate CSS Gradient Generator: http://www.colorzilla.com/gradient-editor/ */
|
||||
background: #242424; /* Old browsers */
|
||||
background: -moz-linear-gradient(top, #242424 50%, #1f1f1f 50%, #171717 100%); /* FF3.6+ */
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(50%,#242424), color-stop(50%,#1f1f1f), color-stop(100%,#171717)); /* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* Chrome10+,Safari5.1+ */
|
||||
background: -o-linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* Opera11.10+ */
|
||||
background: -ms-linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* IE10+ */
|
||||
/* Filter was causing a lot of weird issues in IE. Elements would stop showing up, or other styles would break. */
|
||||
/*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#242424', endColorstr='#171717',GradientType=0 );*/ /* IE6-9 */
|
||||
background: linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* W3C */
|
||||
|
||||
/* Start hidden and with 0 opacity. Opacity is used to fade in modern browsers. */
|
||||
/* Can't use display block to hide initially because widths of slider handles aren't calculated and avaialbe for positioning correctly. */
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* General styles for individual controls. */
|
||||
.vjs-default-skin .vjs-control {
|
||||
position: relative; float: left;
|
||||
text-align: center; margin: 0; padding: 0;
|
||||
height: 2.6em; width: 2.6em;
|
||||
}
|
||||
|
||||
.vjs-default-skin .vjs-control:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Hide control text visually, but have it available for screenreaders: h5bp.com/v */
|
||||
.vjs-default-skin .vjs-control-text { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
|
||||
|
||||
|
||||
/* Play/Pause
|
||||
-------------------------------------------------------------------------------- */
|
||||
.vjs-default-skin .vjs-play-control { width: 5em; cursor: pointer !important; }
|
||||
/* Play Icon */
|
||||
.vjs-default-skin.vjs-paused .vjs-play-control div { width: 15px; height: 17px; background: url('video-js.png'); margin: 0.5em auto 0; }
|
||||
.vjs-default-skin.vjs-playing .vjs-play-control div { width: 15px; height: 17px; background: url('video-js.png') -25px 0; margin: 0.5em auto 0; }
|
||||
|
||||
/* Rewind
|
||||
-------------------------------------------------------------------------------- */
|
||||
.vjs-default-skin .vjs-rewind-control { width: 5em; cursor: pointer !important; }
|
||||
.vjs-default-skin .vjs-rewind-control div { width: 19px; height: 16px; background: url('video-js.png'); margin: 0.5em auto 0; }
|
||||
|
||||
/* Volume/Mute
|
||||
-------------------------------------------------------------------------------- */
|
||||
.vjs-default-skin .vjs-mute-control { width: 3.8em; cursor: pointer !important; float: right; }
|
||||
.vjs-default-skin .vjs-mute-control div { width: 22px; height: 16px; background: url('video-js.png') -75px -25px; margin: 0.5em auto 0; }
|
||||
.vjs-default-skin .vjs-mute-control.vjs-vol-0 div { background: url('video-js.png') 0 -25px; }
|
||||
.vjs-default-skin .vjs-mute-control.vjs-vol-1 div { background: url('video-js.png') -25px -25px; }
|
||||
.vjs-default-skin .vjs-mute-control.vjs-vol-2 div { background: url('video-js.png') -50px -25px; }
|
||||
|
||||
|
||||
.vjs-default-skin .vjs-volume-control { width: 5em; float: right; }
|
||||
.vjs-default-skin .vjs-volume-bar {
|
||||
position: relative; width: 5em; height: 0.6em; margin: 1em auto 0; cursor: pointer !important;
|
||||
|
||||
-moz-border-radius: 0.3em; -webkit-border-radius: 0.3em; border-radius: 0.3em;
|
||||
|
||||
background: #666;
|
||||
background: -moz-linear-gradient(top, #333, #666);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#333), to(#666));
|
||||
background: -webkit-linear-gradient(top, #333, #666);
|
||||
background: -o-linear-gradient(top, #333, #666);
|
||||
background: -ms-linear-gradient(top, #333, #666);
|
||||
background: linear-gradient(top, #333, #666);
|
||||
}
|
||||
.vjs-default-skin .vjs-volume-level {
|
||||
position: absolute; top: 0; left: 0; height: 0.6em;
|
||||
|
||||
-moz-border-radius: 0.3em; -webkit-border-radius: 0.3em; border-radius: 0.3em;
|
||||
|
||||
background: #fff;
|
||||
background: -moz-linear-gradient(top, #fff, #ccc);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ccc));
|
||||
background: -webkit-linear-gradient(top, #fff, #ccc);
|
||||
background: -o-linear-gradient(top, #fff, #ccc);
|
||||
background: -ms-linear-gradient(top, #fff, #ccc);
|
||||
background: linear-gradient(top, #fff, #ccc);
|
||||
}
|
||||
.vjs-default-skin .vjs-volume-handle {
|
||||
position: absolute; top: -0.2em; width: 0.8em; height: 0.8em; background: #ccc; left: 0;
|
||||
border: 1px solid #fff;
|
||||
-moz-border-radius: 0.6em; -webkit-border-radius: 0.6em; border-radius: 0.6em;
|
||||
}
|
||||
|
||||
/* Progress
|
||||
-------------------------------------------------------------------------------- */
|
||||
.vjs-default-skin div.vjs-progress-control {
|
||||
position: absolute;
|
||||
left: 4.8em; right: 4.8em; /* Leave room for time displays. */
|
||||
height: 1.0em; width: auto;
|
||||
top: -1.3em; /* Set above the rest of the controls. And leave room for 2px of borders (progress bottom and controls top). */
|
||||
border-bottom: 1px solid #1F1F1F;
|
||||
border-top: 1px solid #222;
|
||||
|
||||
/* CSS Gradient */
|
||||
background: #333;
|
||||
background: -moz-linear-gradient(top, #222, #333);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#222), to(#333));
|
||||
background: -webkit-linear-gradient(top, #222, #333);
|
||||
background: -o-linear-gradient(top, #333, #222);
|
||||
background: -ms-linear-gradient(top, #333, #222);
|
||||
background: linear-gradient(top, #333, #222);
|
||||
|
||||
|
||||
/* 1px top shadow */
|
||||
/* -webkit-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15);*/
|
||||
}
|
||||
|
||||
/* Box containing play and load progresses. Also acts as seek scrubber. */
|
||||
.vjs-default-skin .vjs-progress-holder {
|
||||
position: relative; cursor: pointer !important; /*overflow: hidden;*/
|
||||
padding: 0; margin: 0; /* Placement within the progress control item */
|
||||
height: 1.0em;
|
||||
-moz-border-radius: 0.6em; -webkit-border-radius: 0.6em; border-radius: 0.6em;
|
||||
|
||||
/* CSS Gradient */
|
||||
background: #111;
|
||||
background: -moz-linear-gradient(top, #111, #262626);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#111), to(#262626));
|
||||
background: -webkit-linear-gradient(top, #111, #262626);
|
||||
background: -o-linear-gradient(top, #111, #262626);
|
||||
background: -ms-linear-gradient(top, #111, #262626);
|
||||
background: linear-gradient(top, #111, #262626);
|
||||
}
|
||||
.vjs-default-skin .vjs-progress-holder .vjs-play-progress,
|
||||
.vjs-default-skin .vjs-progress-holder .vjs-load-progress { /* Progress Bars */
|
||||
position: absolute; display: block; height: 1.0em; margin: 0; padding: 0;
|
||||
left: 0; top: 0; /*Needed for IE6*/
|
||||
-moz-border-radius: 0.6em; -webkit-border-radius: 0.6em; border-radius: 0.6em;
|
||||
|
||||
/*width: 0;*/
|
||||
}
|
||||
|
||||
.vjs-default-skin .vjs-play-progress {
|
||||
/* CSS Gradient. */
|
||||
background: #fff; /* Old browsers */
|
||||
background: -moz-linear-gradient(top, #fff 0%, #d6d6d6 50%, #fff 100%);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%,#fff), color-stop(50%,#d6d6d6), color-stop(100%,#fff));
|
||||
background: -webkit-linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
|
||||
background: -o-linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
|
||||
background: -ms-linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
|
||||
background: linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
|
||||
|
||||
background: #efefef;
|
||||
background: -moz-linear-gradient(top, #efefef 0%, #f5f5f5 50%, #dbdbdb 50%, #f1f1f1 100%);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%,#efefef), color-stop(50%,#f5f5f5), color-stop(50%,#dbdbdb), color-stop(100%,#f1f1f1));
|
||||
background: -webkit-linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
|
||||
background: -o-linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
|
||||
background: -ms-linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#efefef', endColorstr='#f1f1f1',GradientType=0 );
|
||||
background: linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
|
||||
}
|
||||
.vjs-default-skin .vjs-load-progress {
|
||||
opacity: 0.8;
|
||||
|
||||
/* CSS Gradient */
|
||||
background: #666;
|
||||
background: -moz-linear-gradient(top, #666, #333);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#666), to(#333));
|
||||
background: -webkit-linear-gradient(top, #666, #333);
|
||||
background: -o-linear-gradient(top, #666, #333);
|
||||
background: -ms-linear-gradient(top, #666, #333);
|
||||
background: linear-gradient(top, #666, #333);
|
||||
}
|
||||
|
||||
.vjs-default-skin div.vjs-seek-handle {
|
||||
position: absolute;
|
||||
width: 16px; height: 16px; /* Match img pixles */
|
||||
margin-top: -0.3em;
|
||||
left: 0; top: 0; /*Needed for IE6*/
|
||||
|
||||
background: url('video-js.png') 0 -50px;
|
||||
/* CSS Curved Corners. Needed to make shadows curved. */
|
||||
-moz-border-radius: 0.8em; -webkit-border-radius: 0.8em; border-radius: 0.8em;
|
||||
/* CSS Shadows */
|
||||
-webkit-box-shadow: 0 2px 4px 0 #000; -moz-box-shadow: 0 2px 4px 0 #000; box-shadow: 0 2px 4px 0 #000;
|
||||
}
|
||||
/* Time Display
|
||||
-------------------------------------------------------------------------------- */
|
||||
.vjs-default-skin .vjs-time-controls {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 1.0em; width: 4.8em;
|
||||
top: -1.3em;
|
||||
border-bottom: 1px solid #1F1F1F;
|
||||
border-top: 1px solid #222;
|
||||
background-color: #333;
|
||||
|
||||
font-size: 1em; line-height: 1.0em; font-weight: normal; font-family: Helvetica, Arial, sans-serif;
|
||||
|
||||
background: #333;
|
||||
background: -moz-linear-gradient(top, #222, #333);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#222), to(#333));
|
||||
background: -webkit-linear-gradient(top, #222, #333);
|
||||
background: -o-linear-gradient(top, #333, #222);
|
||||
background: -ms-linear-gradient(top, #333, #222);
|
||||
background: linear-gradient(top, #333, #222);
|
||||
|
||||
/* 1px top shadow */
|
||||
/* -webkit-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15);*/
|
||||
}
|
||||
|
||||
.vjs-default-skin .vjs-current-time { left: 0; }
|
||||
|
||||
.vjs-default-skin .vjs-duration { right: 0; display: none; }
|
||||
.vjs-default-skin .vjs-remaining-time { right: 0; }
|
||||
|
||||
.vjs-time-divider { display:none; }
|
||||
|
||||
.vjs-default-skin .vjs-time-control { font-size: 1em; line-height: 1; font-weight: normal; font-family: Helvetica, Arial, sans-serif; }
|
||||
.vjs-default-skin .vjs-time-control span { line-height: 25px; /* Centering vertically */ }
|
||||
|
||||
/* Fullscreen
|
||||
-------------------------------------------------------------------------------- */
|
||||
.vjs-secondary-controls { float: right; }
|
||||
|
||||
.vjs-default-skin .vjs-fullscreen-control { width: 3.8em; cursor: pointer !important; float: right; }
|
||||
.vjs-default-skin .vjs-fullscreen-control div { width: 16px; height: 16px; background: url('video-js.png') -50px 0; margin: 0.5em auto 0; }
|
||||
|
||||
.vjs-default-skin.vjs-fullscreen .vjs-fullscreen-control div { background: url('video-js.png') -75px 0; }
|
||||
|
||||
|
||||
/* Big Play Button (at start)
|
||||
---------------------------------------------------------*/
|
||||
.vjs-default-skin .vjs-big-play-button {
|
||||
display: block; /* Start hidden */ z-index: 2;
|
||||
position: absolute; top: 50%; left: 50%; width: 8.0em; height: 8.0em; margin: -43px 0 0 -43px; text-align: center; vertical-align: center; cursor: pointer !important;
|
||||
border: 0.3em solid #fff; opacity: 0.95;
|
||||
-webkit-border-radius: 25px; -moz-border-radius: 25px; border-radius: 25px;
|
||||
|
||||
background: #454545;
|
||||
background: -moz-linear-gradient(top, #454545 0%, #232323 50%, #161616 50%, #3f3f3f 100%);
|
||||
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%,#454545), color-stop(50%,#232323), color-stop(50%,#161616), color-stop(100%,#3f3f3f));
|
||||
background: -webkit-linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
|
||||
background: -o-linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
|
||||
background: -ms-linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#454545', endColorstr='#3f3f3f',GradientType=0 );
|
||||
background: linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
|
||||
|
||||
/* CSS Shadows */
|
||||
-webkit-box-shadow: 4px 4px 8px #000; -moz-box-shadow: 4px 4px 8px #000; box-shadow: 4px 4px 8px #000;
|
||||
}
|
||||
|
||||
.vjs-default-skin div.vjs-big-play-button:hover {
|
||||
-webkit-box-shadow: 0 0 80px #fff; -moz-box-shadow: 0 0 80px #fff; box-shadow: 0 0 80px #fff;
|
||||
}
|
||||
|
||||
.vjs-default-skin div.vjs-big-play-button span {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
display: block; width: 35px; height: 42px;
|
||||
margin: -20px 0 0 -15px; /* Using negative margin to center image. */
|
||||
background: url('video-js.png') -100px 0;
|
||||
}
|
||||
|
||||
/* Loading Spinner
|
||||
---------------------------------------------------------*/
|
||||
/* CSS Spinners by Kilian Valkhof - http://kilianvalkhof.com/2010/css-xhtml/css3-loading-spinners-without-images/ */
|
||||
.vjs-loading-spinner {
|
||||
display: none;
|
||||
position: absolute; top: 50%; left: 50%; width: 55px; height: 55px;
|
||||
margin: -28px 0 0 -28px;
|
||||
-webkit-animation-name: rotatethis;
|
||||
-webkit-animation-duration:1s;
|
||||
-webkit-animation-iteration-count:infinite;
|
||||
-webkit-animation-timing-function:linear;
|
||||
-moz-animation-name: rotatethis;
|
||||
-moz-animation-duration:1s;
|
||||
-moz-animation-iteration-count:infinite;
|
||||
-moz-animation-timing-function:linear;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotatethis {
|
||||
0% {-webkit-transform:scale(0.6) rotate(0deg); }
|
||||
12.5% {-webkit-transform:scale(0.6) rotate(0deg); }
|
||||
12.51% {-webkit-transform:scale(0.6) rotate(45deg); }
|
||||
25% {-webkit-transform:scale(0.6) rotate(45deg); }
|
||||
25.01% {-webkit-transform:scale(0.6) rotate(90deg);}
|
||||
37.5% {-webkit-transform:scale(0.6) rotate(90deg);}
|
||||
37.51% {-webkit-transform:scale(0.6) rotate(135deg);}
|
||||
50% {-webkit-transform:scale(0.6) rotate(135deg);}
|
||||
50.01% {-webkit-transform:scale(0.6) rotate(180deg);}
|
||||
62.5% {-webkit-transform:scale(0.6) rotate(180deg);}
|
||||
62.51% {-webkit-transform:scale(0.6) rotate(225deg);}
|
||||
75% {-webkit-transform:scale(0.6) rotate(225deg);}
|
||||
75.01% {-webkit-transform:scale(0.6) rotate(270deg);}
|
||||
87.5% {-webkit-transform:scale(0.6) rotate(270deg);}
|
||||
87.51% {-webkit-transform:scale(0.6) rotate(315deg);}
|
||||
100% {-webkit-transform:scale(0.6) rotate(315deg);}
|
||||
}
|
||||
|
||||
@-moz-keyframes rotatethis {
|
||||
0% {-moz-transform:scale(0.6) rotate(0deg);}
|
||||
12.5% {-moz-transform:scale(0.6) rotate(0deg);}
|
||||
12.51% {-moz-transform:scale(0.6) rotate(45deg);}
|
||||
25% {-moz-transform:scale(0.6) rotate(45deg);}
|
||||
25.01% {-moz-transform:scale(0.6) rotate(90deg);}
|
||||
37.5% {-moz-transform:scale(0.6) rotate(90deg);}
|
||||
37.51% {-moz-transform:scale(0.6) rotate(135deg);}
|
||||
50% {-moz-transform:scale(0.6) rotate(135deg);}
|
||||
50.01% {-moz-transform:scale(0.6) rotate(180deg);}
|
||||
62.5% {-moz-transform:scale(0.6) rotate(180deg);}
|
||||
62.51% {-moz-transform:scale(0.6) rotate(225deg);}
|
||||
75% {-moz-transform:scale(0.6) rotate(225deg);}
|
||||
75.01% {-moz-transform:scale(0.6) rotate(270deg);}
|
||||
87.5% {-moz-transform:scale(0.6) rotate(270deg);}
|
||||
87.51% {-moz-transform:scale(0.6) rotate(315deg);}
|
||||
100% {-moz-transform:scale(0.6) rotate(315deg);}
|
||||
}
|
||||
/* Each circle */
|
||||
div.vjs-loading-spinner .ball1 { opacity: 0.12; position:absolute; left: 20px; top: 0px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball2 { opacity: 0.25; position:absolute; left: 34px; top: 6px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball3 { opacity: 0.37; position:absolute; left: 40px; top: 20px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball4 { opacity: 0.50; position:absolute; left: 34px; top: 34px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 10px; -webkit-border-radius: 10px; -moz-border-radius: 15px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball5 { opacity: 0.62; position:absolute; left: 20px; top: 40px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball6 { opacity: 0.75; position:absolute; left: 6px; top: 34px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball7 { opacity: 0.87; position:absolute; left: 0px; top: 20px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
||||
|
||||
div.vjs-loading-spinner .ball8 { opacity: 1.00; position:absolute; left: 6px; top: 6px; width: 13px; height: 13px; background: #fff;
|
||||
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
|
2
extlib/video-js/video-js.min.css
vendored
2
extlib/video-js/video-js.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
File diff suppressed because it is too large
Load Diff
4
extlib/video-js/video.min.js
vendored
4
extlib/video-js/video.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,5 +1,9 @@
|
||||
# If you want to make changes to this file, first copy it to
|
||||
# mediagoblin_local.ini, then make the changes there.
|
||||
#
|
||||
# If you don't see what you need here, have a look at mediagoblin/config_spec.ini
|
||||
# It defines types and defaults so it’s a good place to look for documentation
|
||||
# or to find hidden options that we didn’t tell you about. :)
|
||||
|
||||
[mediagoblin]
|
||||
direct_remote_path = /mgoblin_static/
|
||||
@ -7,7 +11,7 @@ email_sender_address = "notice@mediagoblin.example.org"
|
||||
|
||||
## Uncomment and change to your DB's appropiate setting.
|
||||
## Default is a local sqlite db "mediagoblin.db".
|
||||
# sql_engine = postgresql:///gmg
|
||||
# sql_engine = postgresql:///mediagoblin
|
||||
|
||||
# set to false to enable sending notices
|
||||
email_debug_mode = true
|
||||
@ -16,6 +20,8 @@ email_debug_mode = true
|
||||
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/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
|
||||
@ -40,3 +46,4 @@ base_url = /mgoblin_media/
|
||||
# place plugins here---each in their own subsection of [plugins]. see
|
||||
# documentation for details.
|
||||
[plugins]
|
||||
[[mediagoblin.plugins.geolocation]]
|
||||
|
@ -23,4 +23,4 @@
|
||||
|
||||
# see http://www.python.org/dev/peps/pep-0386/
|
||||
|
||||
__version__ = "0.3.2.dev"
|
||||
__version__ = "0.4.0.dev"
|
||||
|
@ -14,28 +14,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/>.
|
||||
|
||||
from mediagoblin.tools.response import render_to_response, render_404
|
||||
from mediagoblin.db.util import DESCENDING
|
||||
from mediagoblin.decorators import require_active_login
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from mediagoblin.db.models import MediaEntry
|
||||
from mediagoblin.decorators import require_active_login
|
||||
from mediagoblin.tools.response import render_to_response
|
||||
|
||||
@require_active_login
|
||||
def admin_processing_panel(request):
|
||||
'''
|
||||
Show the global processing panel for this instance
|
||||
'''
|
||||
# TODO: Why not a "require_admin_login" decorator throwing a 403 exception?
|
||||
if not request.user.is_admin:
|
||||
return render_404(request)
|
||||
raise Forbidden()
|
||||
|
||||
processing_entries = request.db.MediaEntry.find(
|
||||
{'state': u'processing'}).sort('created', DESCENDING)
|
||||
processing_entries = MediaEntry.query.filter_by(state = u'processing').\
|
||||
order_by(MediaEntry.created.desc())
|
||||
|
||||
# Get media entries which have failed to process
|
||||
failed_entries = request.db.MediaEntry.find(
|
||||
{'state': u'failed'}).sort('created', DESCENDING)
|
||||
failed_entries = MediaEntry.query.filter_by(state = u'failed').\
|
||||
order_by(MediaEntry.created.desc())
|
||||
|
||||
processed_entries = request.db.MediaEntry.find(
|
||||
{'state': u'processed'}).sort('created', DESCENDING).limit(10)
|
||||
processed_entries = MediaEntry.query.filter_by(state = u'processed').\
|
||||
order_by(MediaEntry.created.desc()).limit(10)
|
||||
|
||||
# Render to response
|
||||
return render_to_response(
|
||||
|
@ -17,23 +17,26 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from mediagoblin.routing import url_map, view_functions, add_route
|
||||
from mediagoblin.routing import get_url_map
|
||||
from mediagoblin.tools.routing import endpoint_to_controller
|
||||
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.exceptions import HTTPException, NotFound
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.routing import RequestRedirect
|
||||
|
||||
from mediagoblin import meddleware, __version__
|
||||
from mediagoblin.tools import common, translate, template
|
||||
from mediagoblin.tools.response import render_404
|
||||
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.mg_globals import setup_globals
|
||||
from mediagoblin.init.celery import setup_celery_from_config
|
||||
from mediagoblin.init.plugins import setup_plugins
|
||||
from mediagoblin.init import (get_jinja_loader, get_staticdirector,
|
||||
setup_global_and_app_config, setup_workbench, setup_database,
|
||||
setup_storage, setup_beaker_cache)
|
||||
from mediagoblin.tools.pluginapi import PluginManager
|
||||
setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
|
||||
setup_storage)
|
||||
from mediagoblin.tools.pluginapi import PluginManager, hook_transform
|
||||
from mediagoblin.tools.crypto import setup_crypto
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@ -64,17 +67,25 @@ class MediaGoblinApp(object):
|
||||
# Open and setup the config
|
||||
global_config, app_config = setup_global_and_app_config(config_path)
|
||||
|
||||
setup_crypto()
|
||||
|
||||
##########################################
|
||||
# Setup other connections / useful objects
|
||||
##########################################
|
||||
|
||||
# Setup Session Manager, not needed in celery
|
||||
self.session_manager = session.SessionManager()
|
||||
|
||||
# load all available locales
|
||||
setup_locales()
|
||||
|
||||
# Set up plugins -- need to do this early so that plugins can
|
||||
# affect startup.
|
||||
_log.info("Setting up plugins.")
|
||||
setup_plugins()
|
||||
|
||||
# Set up the database
|
||||
self.connection, self.db = setup_database()
|
||||
self.db = setup_database()
|
||||
|
||||
# Register themes
|
||||
self.theme_registry, self.current_theme = register_themes(app_config)
|
||||
@ -90,18 +101,11 @@ class MediaGoblinApp(object):
|
||||
self.public_store, self.queue_store = setup_storage()
|
||||
|
||||
# set up routing
|
||||
self.url_map = url_map
|
||||
|
||||
for route in PluginManager().get_routes():
|
||||
_log.debug('adding plugin route: {0}'.format(route))
|
||||
add_route(*route)
|
||||
self.url_map = get_url_map()
|
||||
|
||||
# set up staticdirector tool
|
||||
self.staticdirector = get_staticdirector(app_config)
|
||||
|
||||
# set up caching
|
||||
self.cache = setup_beaker_cache()
|
||||
|
||||
# Setup celery, if appropriate
|
||||
if setup_celery and not app_config.get('celery_setup_elsewhere'):
|
||||
if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
|
||||
@ -132,10 +136,8 @@ class MediaGoblinApp(object):
|
||||
def call_backend(self, environ, start_response):
|
||||
request = Request(environ)
|
||||
|
||||
## Compatibility webob -> werkzeug
|
||||
# Compatibility with django, use request.args preferrably
|
||||
request.GET = request.args
|
||||
request.accept_language = request.accept_languages
|
||||
request.accept = request.accept_mimetypes
|
||||
|
||||
## Routing / controller loading stuff
|
||||
map_adapter = self.url_map.bind_to_environ(request.environ)
|
||||
@ -158,7 +160,8 @@ class MediaGoblinApp(object):
|
||||
|
||||
## Attach utilities to the request object
|
||||
# Do we really want to load this via middleware? Maybe?
|
||||
request.session = request.environ['beaker.session']
|
||||
session_manager = self.session_manager
|
||||
request.session = session_manager.load_session_from_cookie(request)
|
||||
# Attach self as request.app
|
||||
# Also attach a few utilities from request.app for convenience?
|
||||
request.app = self
|
||||
@ -185,42 +188,54 @@ class MediaGoblinApp(object):
|
||||
|
||||
mg_request.setup_user_in_request(request)
|
||||
|
||||
request.controller_name = None
|
||||
try:
|
||||
endpoint, url_values = map_adapter.match()
|
||||
found_rule, url_values = map_adapter.match(return_rule=True)
|
||||
request.matchdict = url_values
|
||||
except NotFound as exc:
|
||||
return render_404(request)(environ, start_response)
|
||||
except RequestRedirect as response:
|
||||
# Deal with 301 responses eg due to missing final slash
|
||||
return response(environ, start_response)
|
||||
except HTTPException as exc:
|
||||
# Support legacy webob.exc responses
|
||||
return exc(environ, start_response)
|
||||
# Stop and render exception
|
||||
return render_http_exception(
|
||||
request, exc,
|
||||
exc.get_description(environ))(environ, start_response)
|
||||
|
||||
view_func = view_functions[endpoint]
|
||||
|
||||
_log.debug('endpoint: {0} view_func: {1}'.format(
|
||||
endpoint,
|
||||
view_func))
|
||||
|
||||
# import the endpoint, or if it's already a callable, call that
|
||||
if isinstance(view_func, unicode) \
|
||||
or isinstance(view_func, str):
|
||||
controller = common.import_component(view_func)
|
||||
else:
|
||||
controller = view_func
|
||||
controller = endpoint_to_controller(found_rule)
|
||||
# Make a reference to the controller's symbolic name on the request...
|
||||
# used for lazy context modification
|
||||
request.controller_name = found_rule.endpoint
|
||||
|
||||
# pass the request through our meddleware classes
|
||||
for m in self.meddleware:
|
||||
response = m.process_request(request, controller)
|
||||
if response is not None:
|
||||
return response(environ, start_response)
|
||||
try:
|
||||
for m in self.meddleware:
|
||||
response = m.process_request(request, controller)
|
||||
if response is not None:
|
||||
return response(environ, start_response)
|
||||
except HTTPException as e:
|
||||
return render_http_exception(
|
||||
request, e,
|
||||
e.get_description(environ))(environ, start_response)
|
||||
|
||||
request.start_response = start_response
|
||||
|
||||
# get the response from the controller
|
||||
response = controller(request)
|
||||
# get the Http response from the controller
|
||||
try:
|
||||
response = controller(request)
|
||||
except HTTPException as e:
|
||||
response = render_http_exception(
|
||||
request, e, e.get_description(environ))
|
||||
|
||||
# pass the response through the meddleware
|
||||
for m in self.meddleware[::-1]:
|
||||
m.process_response(request, response)
|
||||
# pass the response through the meddlewares
|
||||
try:
|
||||
for m in self.meddleware[::-1]:
|
||||
m.process_response(request, response)
|
||||
except HTTPException as e:
|
||||
response = render_http_exception(
|
||||
request, e, e.get_description(environ))
|
||||
|
||||
session_manager.save_session_to_cookie(request.session,
|
||||
request, response)
|
||||
|
||||
return response(environ, start_response)
|
||||
|
||||
@ -248,5 +263,6 @@ def paste_app_factory(global_config, **app_config):
|
||||
raise IOError("Usable mediagoblin config not found.")
|
||||
|
||||
mgoblin_app = MediaGoblinApp(mediagoblin_config)
|
||||
mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
|
||||
|
||||
return mgoblin_app
|
||||
|
@ -15,54 +15,76 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import wtforms
|
||||
import re
|
||||
|
||||
from mediagoblin.tools.translate import fake_ugettext_passthrough as _
|
||||
from mediagoblin.tools.mail import normalize_email
|
||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||
|
||||
def normalize_user_or_email_field(allow_email=True, allow_user=True):
|
||||
"""Check if we were passed a field that matches a username and/or email pattern
|
||||
|
||||
This is useful for fields that can take either a username or email
|
||||
address. Use the parameters if you want to only allow a username for
|
||||
instance"""
|
||||
message = _(u'Invalid User name or email address.')
|
||||
nomail_msg = _(u"This field does not take email addresses.")
|
||||
nouser_msg = _(u"This field requires an email address.")
|
||||
|
||||
def _normalize_field(form, field):
|
||||
email = u'@' in field.data
|
||||
if email: # normalize email address casing
|
||||
if not allow_email:
|
||||
raise wtforms.ValidationError(nomail_msg)
|
||||
wtforms.validators.Email()(form, field)
|
||||
field.data = normalize_email(field.data)
|
||||
else: # lower case user names
|
||||
if not allow_user:
|
||||
raise wtforms.ValidationError(nouser_msg)
|
||||
wtforms.validators.Length(min=3, max=30)(form, field)
|
||||
wtforms.validators.Regexp(r'^\w+$')(form, field)
|
||||
field.data = field.data.lower()
|
||||
if field.data is None: # should not happen, but be cautious anyway
|
||||
raise wtforms.ValidationError(message)
|
||||
return _normalize_field
|
||||
|
||||
|
||||
class RegistrationForm(wtforms.Form):
|
||||
username = wtforms.TextField(
|
||||
_('Username'),
|
||||
[wtforms.validators.Required(),
|
||||
wtforms.validators.Length(min=3, max=30),
|
||||
wtforms.validators.Regexp(r'^\w+$')])
|
||||
normalize_user_or_email_field(allow_email=False)])
|
||||
password = wtforms.PasswordField(
|
||||
_('Password'),
|
||||
[wtforms.validators.Required(),
|
||||
wtforms.validators.Length(min=6, max=30)])
|
||||
wtforms.validators.Length(min=5, max=1024)])
|
||||
email = wtforms.TextField(
|
||||
_('Email address'),
|
||||
[wtforms.validators.Required(),
|
||||
wtforms.validators.Email()])
|
||||
normalize_user_or_email_field(allow_user=False)])
|
||||
|
||||
|
||||
class LoginForm(wtforms.Form):
|
||||
username = wtforms.TextField(
|
||||
_('Username'),
|
||||
_('Username or Email'),
|
||||
[wtforms.validators.Required(),
|
||||
wtforms.validators.Regexp(r'^\w+$')])
|
||||
normalize_user_or_email_field()])
|
||||
password = wtforms.PasswordField(
|
||||
_('Password'),
|
||||
[wtforms.validators.Required()])
|
||||
[wtforms.validators.Required(),
|
||||
wtforms.validators.Length(min=5, max=1024)])
|
||||
|
||||
|
||||
class ForgotPassForm(wtforms.Form):
|
||||
username = wtforms.TextField(
|
||||
_('Username or email'),
|
||||
[wtforms.validators.Required()])
|
||||
|
||||
def validate_username(form, field):
|
||||
if not (re.match(r'^\w+$', field.data) or
|
||||
re.match(r'^.+@[^.].*\.[a-z]{2,10}$', field.data,
|
||||
re.IGNORECASE)):
|
||||
raise wtforms.ValidationError(_(u'Incorrect input'))
|
||||
[wtforms.validators.Required(),
|
||||
normalize_user_or_email_field()])
|
||||
|
||||
|
||||
class ChangePassForm(wtforms.Form):
|
||||
password = wtforms.PasswordField(
|
||||
'Password',
|
||||
[wtforms.validators.Required(),
|
||||
wtforms.validators.Length(min=6, max=30)])
|
||||
wtforms.validators.Length(min=5, max=1024)])
|
||||
userid = wtforms.HiddenField(
|
||||
'',
|
||||
[wtforms.validators.Required()])
|
||||
|
@ -109,7 +109,7 @@ def send_verification_email(user, request):
|
||||
'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
|
||||
host=request.host,
|
||||
uri=request.urlgen('mediagoblin.auth.verify_email'),
|
||||
userid=unicode(user._id),
|
||||
userid=unicode(user.id),
|
||||
verification_key=user.verification_key)})
|
||||
|
||||
# TODO: There is no error handling in place
|
||||
@ -144,7 +144,7 @@ def send_fp_verification_email(user, request):
|
||||
'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format(
|
||||
host=request.host,
|
||||
uri=request.urlgen('mediagoblin.auth.verify_forgot_password'),
|
||||
userid=unicode(user._id),
|
||||
userid=unicode(user.id),
|
||||
fp_verification_key=user.fp_verification_key)})
|
||||
|
||||
# TODO: There is no error handling in place
|
||||
|
@ -17,18 +17,15 @@
|
||||
import uuid
|
||||
import datetime
|
||||
|
||||
from webob import exc
|
||||
|
||||
from mediagoblin import messages
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin import messages, mg_globals
|
||||
from mediagoblin.db.models import User
|
||||
from mediagoblin.tools.response import render_to_response, redirect, render_404
|
||||
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||
from mediagoblin.db.util import ObjectId, InvalidId
|
||||
from mediagoblin.auth import lib as auth_lib
|
||||
from mediagoblin.auth import forms as auth_forms
|
||||
from mediagoblin.auth.lib import send_verification_email, \
|
||||
send_fp_verification_email
|
||||
|
||||
from sqlalchemy import or_
|
||||
|
||||
def email_debug_message(request):
|
||||
"""
|
||||
@ -44,8 +41,10 @@ def email_debug_message(request):
|
||||
|
||||
|
||||
def register(request):
|
||||
"""
|
||||
Your classic registration view!
|
||||
"""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"]:
|
||||
@ -59,14 +58,8 @@ def register(request):
|
||||
|
||||
if request.method == 'POST' and register_form.validate():
|
||||
# TODO: Make sure the user doesn't exist already
|
||||
username = unicode(request.form['username'].lower())
|
||||
em_user, em_dom = unicode(request.form['email']).split("@", 1)
|
||||
em_dom = em_dom.lower()
|
||||
email = em_user + "@" + em_dom
|
||||
users_with_username = request.db.User.find(
|
||||
{'username': username}).count()
|
||||
users_with_email = request.db.User.find(
|
||||
{'email': email}).count()
|
||||
users_with_username = User.query.filter_by(username=register_form.data['username']).count()
|
||||
users_with_email = User.query.filter_by(email=register_form.data['email']).count()
|
||||
|
||||
extra_validation_passes = True
|
||||
|
||||
@ -81,16 +74,16 @@ def register(request):
|
||||
|
||||
if extra_validation_passes:
|
||||
# Create the user
|
||||
user = request.db.User()
|
||||
user.username = username
|
||||
user.email = email
|
||||
user = User()
|
||||
user.username = register_form.data['username']
|
||||
user.email = register_form.data['email']
|
||||
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
|
||||
request.form['password'])
|
||||
register_form.password.data)
|
||||
user.verification_key = unicode(uuid.uuid4())
|
||||
user.save(validate=True)
|
||||
user.save()
|
||||
|
||||
# log the user in
|
||||
request.session['user_id'] = unicode(user._id)
|
||||
request.session['user_id'] = unicode(user.id)
|
||||
request.session.save()
|
||||
|
||||
# send verification email
|
||||
@ -119,21 +112,29 @@ def login(request):
|
||||
|
||||
login_failed = False
|
||||
|
||||
if request.method == 'POST' and login_form.validate():
|
||||
user = request.db.User.find_one(
|
||||
{'username': request.form['username'].lower()})
|
||||
if request.method == 'POST':
|
||||
|
||||
username = login_form.data['username']
|
||||
|
||||
if user and user.check_login(request.form['password']):
|
||||
# set up login in session
|
||||
request.session['user_id'] = unicode(user._id)
|
||||
request.session.save()
|
||||
if login_form.validate():
|
||||
user = User.query.filter(
|
||||
or_(
|
||||
User.username == username,
|
||||
User.email == username,
|
||||
|
||||
if request.form.get('next'):
|
||||
return exc.HTTPFound(location=request.form['next'])
|
||||
else:
|
||||
return redirect(request, "index")
|
||||
)).first()
|
||||
|
||||
else:
|
||||
if user and user.check_login(login_form.password.data):
|
||||
# set up login in session
|
||||
request.session['user_id'] = unicode(user.id)
|
||||
request.session.save()
|
||||
|
||||
if request.form.get('next'):
|
||||
return redirect(request, location=request.form['next'])
|
||||
else:
|
||||
return redirect(request, "index")
|
||||
|
||||
# Some failure during login occured if we are here!
|
||||
# Prevent detecting who's on this system by testing login
|
||||
# attempt timings
|
||||
auth_lib.fake_login_attempt()
|
||||
@ -166,8 +167,7 @@ def verify_email(request):
|
||||
if not 'userid' in request.GET or not 'token' in request.GET:
|
||||
return render_404(request)
|
||||
|
||||
user = request.db.User.find_one(
|
||||
{'_id': ObjectId(unicode(request.GET['userid']))})
|
||||
user = User.query.filter_by(id=request.args['userid']).first()
|
||||
|
||||
if user and user.verification_key == unicode(request.GET['token']):
|
||||
user.status = u'active'
|
||||
@ -204,7 +204,7 @@ def resend_activation(request):
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You must be logged in so we know who to send the email to!'))
|
||||
|
||||
|
||||
return redirect(request, 'mediagoblin.auth.login')
|
||||
|
||||
if request.user.email_verified:
|
||||
@ -212,12 +212,12 @@ def resend_activation(request):
|
||||
request,
|
||||
messages.ERROR,
|
||||
_("You've already verified your email address!"))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@ -234,61 +234,66 @@ def forgot_password(request):
|
||||
"""
|
||||
Forgot password view
|
||||
|
||||
Sends an email with an url to renew forgotten password
|
||||
Sends an email with an url to renew forgotten password.
|
||||
Use GET querystring parameter 'username' to pre-populate the input field
|
||||
"""
|
||||
fp_form = auth_forms.ForgotPassForm(request.form,
|
||||
username=request.GET.get('username'))
|
||||
username=request.args.get('username'))
|
||||
|
||||
if request.method == 'POST' and fp_form.validate():
|
||||
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})
|
||||
|
||||
# '$or' not available till mongodb 1.5.3
|
||||
user = request.db.User.find_one(
|
||||
{'username': request.form['username']})
|
||||
if not user:
|
||||
user = request.db.User.find_one(
|
||||
{'email': request.form['username']})
|
||||
# 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
|
||||
# not reveal if the operation was successful then as we don't want to
|
||||
# leak if an email address exists in the system.
|
||||
found_by_email = '@' in fp_form.username.data
|
||||
|
||||
if user:
|
||||
if user.email_verified and user.status == 'active':
|
||||
user.fp_verification_key = unicode(uuid.uuid4())
|
||||
user.fp_token_expire = datetime.datetime.now() + \
|
||||
datetime.timedelta(days=10)
|
||||
user.save()
|
||||
if found_by_email:
|
||||
user = User.query.filter_by(
|
||||
email = fp_form.username.data).first()
|
||||
# Don't reveal success in case the lookup happened by email address.
|
||||
success_message=_("If that email address (case sensitive!) is "
|
||||
"registered an email has been sent with instructions "
|
||||
"on how to change your password.")
|
||||
|
||||
send_fp_verification_email(user, request)
|
||||
else: # found by username
|
||||
user = User.query.filter_by(
|
||||
username = fp_form.username.data).first()
|
||||
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.INFO,
|
||||
_("An email has been sent with instructions on how to "
|
||||
"change your password."))
|
||||
email_debug_message(request)
|
||||
|
||||
else:
|
||||
# special case... we can't send the email because the
|
||||
# username is inactive / hasn't verified their email
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_("Could not send password recovery email as "
|
||||
"your username is inactive or your account's "
|
||||
"email address has not been verified."))
|
||||
|
||||
return redirect(
|
||||
request, 'mediagoblin.user_pages.user_home',
|
||||
user=user.username)
|
||||
return redirect(request, 'mediagoblin.auth.login')
|
||||
else:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_("Couldn't find someone with that username or email."))
|
||||
if user is None:
|
||||
messages.add_message(request,
|
||||
messages.WARNING,
|
||||
_("Couldn't find someone with that username."))
|
||||
return redirect(request, 'mediagoblin.auth.forgot_password')
|
||||
|
||||
return render_to_response(
|
||||
request,
|
||||
'mediagoblin/auth/forgot_password.html',
|
||||
{'fp_form': fp_form})
|
||||
success_message=_("An email has been sent with instructions "
|
||||
"on how to change your password.")
|
||||
|
||||
if user and not(user.email_verified and user.status == 'active'):
|
||||
# Don't send reminder because user is inactive or has no verified email
|
||||
messages.add_message(request,
|
||||
messages.WARNING,
|
||||
_("Could not send password recovery email as your username is in"
|
||||
"active or your account's email address has not been verified."))
|
||||
|
||||
return redirect(request, 'mediagoblin.user_pages.user_home',
|
||||
user=user.username)
|
||||
|
||||
# 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)
|
||||
|
||||
messages.add_message(request, messages.INFO, success_message)
|
||||
return redirect(request, 'mediagoblin.auth.login')
|
||||
|
||||
|
||||
def verify_forgot_password(request):
|
||||
@ -305,11 +310,9 @@ def verify_forgot_password(request):
|
||||
formdata_userid = formdata['vars']['userid']
|
||||
formdata_vars = formdata['vars']
|
||||
|
||||
# check if it's a valid Id
|
||||
try:
|
||||
user = request.db.User.find_one(
|
||||
{'_id': ObjectId(unicode(formdata_userid))})
|
||||
except InvalidId:
|
||||
# check if it's a valid user id
|
||||
user = User.query.filter_by(id=formdata_userid).first()
|
||||
if not user:
|
||||
return render_404(request)
|
||||
|
||||
# check if we have a real user and correct token
|
||||
@ -322,7 +325,7 @@ def verify_forgot_password(request):
|
||||
|
||||
if request.method == 'POST' and cp_form.validate():
|
||||
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
|
||||
request.form['password'])
|
||||
cp_form.password.data)
|
||||
user.fp_verification_key = None
|
||||
user.fp_token_expire = None
|
||||
user.save()
|
||||
@ -338,7 +341,7 @@ def verify_forgot_password(request):
|
||||
'mediagoblin/auth/change_fp.html',
|
||||
{'cp_form': cp_form})
|
||||
|
||||
# in case there is a valid id but no user whit that id in the db
|
||||
# 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)
|
||||
|
@ -9,14 +9,14 @@ source_link = string(default="https://gitorious.org/mediagoblin/mediagoblin")
|
||||
media_types = string_list(default=list("mediagoblin.media_types.image"))
|
||||
|
||||
# database stuff
|
||||
db_host = string()
|
||||
db_name = string(default="mediagoblin")
|
||||
db_port = integer()
|
||||
sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db")
|
||||
|
||||
# Where temporary files used in processing and etc are kept
|
||||
workbench_path = string(default="%(here)s/user_dev/media/workbench")
|
||||
|
||||
# Where to store cryptographic sensible data
|
||||
crypto_path = string(default="%(here)s/user_dev/crypto")
|
||||
|
||||
# Where mediagoblin-builtin static assets are kept
|
||||
direct_remote_path = string(default="/mgoblin_static/")
|
||||
|
||||
@ -32,7 +32,10 @@ email_smtp_pass = string(default=None)
|
||||
allow_registration = boolean(default=True)
|
||||
|
||||
# tag parsing
|
||||
tags_max_length = integer(default=50)
|
||||
tags_max_length = integer(default=255)
|
||||
|
||||
# Enable/disable comments
|
||||
allow_comments = boolean(default=True)
|
||||
|
||||
# Whether comments are ascending or descending
|
||||
comments_ascending = boolean(default=True)
|
||||
@ -58,7 +61,7 @@ csrf_cookie_name = string(default='mediagoblin_csrftoken')
|
||||
push_urls = string_list(default=list())
|
||||
|
||||
exif_visible = boolean(default=False)
|
||||
geolocation_map_visible = boolean(default=False)
|
||||
original_date_visible = boolean(default=False)
|
||||
|
||||
# Theming stuff
|
||||
theme_install_dir = string(default="%(here)s/user_dev/themes/")
|
||||
@ -89,6 +92,12 @@ max_height = integer(default=640)
|
||||
max_width = integer(default=180)
|
||||
max_height = integer(default=180)
|
||||
|
||||
[media_type:mediagoblin.media_types.image]
|
||||
# One of BICUBIC, BILINEAR, NEAREST, ANTIALIAS
|
||||
resize_filter = string(default="ANTIALIAS")
|
||||
#level of compression used when resizing images
|
||||
quality = integer(default=90)
|
||||
|
||||
[media_type:mediagoblin.media_types.video]
|
||||
# Should we keep the original file?
|
||||
keep_original = boolean(default=False)
|
||||
@ -100,22 +109,28 @@ vp8_quality = integer(default=8)
|
||||
# Range: -0.1..1
|
||||
vorbis_quality = float(default=0.3)
|
||||
|
||||
# Autoplay the video when page is loaded?
|
||||
auto_play = boolean(default=True)
|
||||
|
||||
[[skip_transcode]]
|
||||
mime_types = string_list(default=list("video/webm"))
|
||||
container_formats = string_list(default=list("Matroska"))
|
||||
video_codecs = string_list(default=list("VP8 video"))
|
||||
audio_codecs = string_list(default=list("Vorbis"))
|
||||
dimensions_match = boolean(default=True)
|
||||
|
||||
[media_type:mediagoblin.media_types.audio]
|
||||
keep_original = boolean(default=True)
|
||||
# vorbisenc qualiy
|
||||
# vorbisenc quality
|
||||
quality = float(default=0.3)
|
||||
create_spectrogram = boolean(default=True)
|
||||
spectrogram_fft_size = integer(default=4096)
|
||||
|
||||
|
||||
[media_type:mediagoblin.media_types.ascii]
|
||||
thumbnail_font = string(default=None)
|
||||
|
||||
[beaker.cache]
|
||||
type = string(default="file")
|
||||
data_dir = string(default="%(here)s/user_dev/beaker/cache/data")
|
||||
lock_dir = string(default="%(here)s/user_dev/beaker/cache/lock")
|
||||
[media_type:mediagoblin.media_types.pdf]
|
||||
pdf_js = boolean(default=False)
|
||||
|
||||
|
||||
[celery]
|
||||
|
@ -18,18 +18,6 @@
|
||||
Database Abstraction/Wrapper Layer
|
||||
==================================
|
||||
|
||||
**NOTE from Chris Webber:** I asked Elrond to explain why he put
|
||||
ASCENDING and DESCENDING in db/util.py when we could just import from
|
||||
pymongo. Read beow for why, but note that nobody is actually doing
|
||||
this and there's no proof that we'll ever support more than
|
||||
MongoDB... it would be a huge amount of work to do so.
|
||||
|
||||
If you really want to prove that possible, jump on IRC and talk to
|
||||
us about making such a branch. In the meanwhile, it doesn't hurt to
|
||||
have things as they are... if it ever makes it hard for us to
|
||||
actually do things, we might revisit or remove this. But for more
|
||||
information, read below.
|
||||
|
||||
This submodule is for most of the db specific stuff.
|
||||
|
||||
There are two main ideas here:
|
||||
|
@ -17,47 +17,19 @@
|
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, object_session
|
||||
from sqlalchemy.orm.query import Query
|
||||
from sqlalchemy.sql.expression import desc
|
||||
from mediagoblin.db.sql.fake import DESCENDING
|
||||
|
||||
|
||||
def _get_query_model(query):
|
||||
cols = query.column_descriptions
|
||||
assert len(cols) == 1, "These functions work only on simple queries"
|
||||
return cols[0]["type"]
|
||||
|
||||
|
||||
class GMGQuery(Query):
|
||||
def sort(self, key, direction):
|
||||
key_col = getattr(_get_query_model(self), key)
|
||||
if direction is DESCENDING:
|
||||
key_col = desc(key_col)
|
||||
return self.order_by(key_col)
|
||||
|
||||
def skip(self, amount):
|
||||
return self.offset(amount)
|
||||
|
||||
|
||||
Session = scoped_session(sessionmaker(query_cls=GMGQuery))
|
||||
|
||||
|
||||
def _fix_query_dict(query_dict):
|
||||
if '_id' in query_dict:
|
||||
query_dict['id'] = query_dict.pop('_id')
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
|
||||
class GMGTableBase(object):
|
||||
query = Session.query_property()
|
||||
|
||||
@classmethod
|
||||
def find(cls, query_dict={}):
|
||||
_fix_query_dict(query_dict)
|
||||
def find(cls, query_dict):
|
||||
return cls.query.filter_by(**query_dict)
|
||||
|
||||
@classmethod
|
||||
def find_one(cls, query_dict={}):
|
||||
_fix_query_dict(query_dict)
|
||||
def find_one(cls, query_dict):
|
||||
return cls.query.filter_by(**query_dict).first()
|
||||
|
||||
@classmethod
|
||||
@ -71,19 +43,20 @@ class GMGTableBase(object):
|
||||
# The key *has* to exist on sql.
|
||||
return getattr(self, key)
|
||||
|
||||
def save(self, validate=True):
|
||||
assert validate
|
||||
def save(self):
|
||||
sess = object_session(self)
|
||||
if sess is None:
|
||||
sess = Session()
|
||||
sess.add(self)
|
||||
sess.commit()
|
||||
|
||||
def delete(self):
|
||||
def delete(self, commit=True):
|
||||
"""Delete the object and commit the change immediately by default"""
|
||||
sess = object_session(self)
|
||||
assert sess is not None, "Not going to delete detached %r" % self
|
||||
sess.delete(self)
|
||||
sess.commit()
|
||||
if commit:
|
||||
sess.commit()
|
||||
|
||||
|
||||
Base = declarative_base(cls=GMGTableBase)
|
@ -14,12 +14,11 @@
|
||||
# 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 sys
|
||||
from mediagoblin.db.sql.base import Session
|
||||
from mediagoblin.db.sql.models import MediaEntry, Tag, MediaTag, Collection
|
||||
|
||||
from mediagoblin.tools.common import simple_printer
|
||||
from sqlalchemy import Table
|
||||
|
||||
class TableAlreadyExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MigrationManager(object):
|
||||
@ -39,7 +38,7 @@ class MigrationManager(object):
|
||||
- migration_registry: where we should find all migrations to
|
||||
run
|
||||
"""
|
||||
self.name = name
|
||||
self.name = unicode(name)
|
||||
self.models = models
|
||||
self.session = session
|
||||
self.migration_registry = migration_registry
|
||||
@ -47,7 +46,7 @@ class MigrationManager(object):
|
||||
self.printer = printer
|
||||
|
||||
# For convenience
|
||||
from mediagoblin.db.sql.models import MigrationData
|
||||
from mediagoblin.db.models import MigrationData
|
||||
|
||||
self.migration_model = MigrationData
|
||||
self.migration_table = MigrationData.__table__
|
||||
@ -132,7 +131,10 @@ class MigrationManager(object):
|
||||
# sanity check before we proceed, none of these should be created
|
||||
for model in self.models:
|
||||
# Maybe in the future just print out a "Yikes!" or something?
|
||||
assert not model.__table__.exists(self.session.bind)
|
||||
if model.__table__.exists(self.session.bind):
|
||||
raise TableAlreadyExists(
|
||||
u"Intended to create table '%s' but it already exists" %
|
||||
model.__table__.name)
|
||||
|
||||
self.migration_model.metadata.create_all(
|
||||
self.session.bind,
|
||||
@ -217,9 +219,9 @@ class MigrationManager(object):
|
||||
u' + Running migration %s, "%s"... ' % (
|
||||
migration_number, migration_func.func_name))
|
||||
migration_func(self.session)
|
||||
self.set_current_migration(migration_number)
|
||||
self.printer('done.\n')
|
||||
|
||||
self.set_current_migration()
|
||||
return u'migrated'
|
||||
|
||||
# Otherwise return None. Well it would do this anyway, but
|
||||
@ -261,67 +263,14 @@ def assure_migrations_table_setup(db):
|
||||
"""
|
||||
Make sure the migrations table is set up in the database.
|
||||
"""
|
||||
from mediagoblin.db.sql.models import MigrationData
|
||||
from mediagoblin.db.models import MigrationData
|
||||
|
||||
if not MigrationData.__table__.exists(db.bind):
|
||||
MigrationData.metadata.create_all(
|
||||
db.bind, tables=[MigrationData.__table__])
|
||||
|
||||
|
||||
##########################
|
||||
# Random utility functions
|
||||
##########################
|
||||
|
||||
|
||||
def atomic_update(table, query_dict, update_values):
|
||||
table.find(query_dict).update(update_values,
|
||||
synchronize_session=False)
|
||||
Session.commit()
|
||||
|
||||
|
||||
def check_media_slug_used(dummy_db, uploader_id, slug, ignore_m_id):
|
||||
filt = (MediaEntry.uploader == uploader_id) \
|
||||
& (MediaEntry.slug == slug)
|
||||
if ignore_m_id is not None:
|
||||
filt = filt & (MediaEntry.id != ignore_m_id)
|
||||
does_exist = Session.query(MediaEntry.id).filter(filt).first() is not None
|
||||
return does_exist
|
||||
|
||||
|
||||
def media_entries_for_tag_slug(dummy_db, tag_slug):
|
||||
return MediaEntry.query \
|
||||
.join(MediaEntry.tags_helper) \
|
||||
.join(MediaTag.tag_helper) \
|
||||
.filter(
|
||||
(MediaEntry.state == u'processed')
|
||||
& (Tag.slug == tag_slug))
|
||||
|
||||
|
||||
def clean_orphan_tags():
|
||||
q1 = Session.query(Tag).outerjoin(MediaTag).filter(MediaTag.id==None)
|
||||
for t in q1:
|
||||
Session.delete(t)
|
||||
|
||||
# The "let the db do all the work" version:
|
||||
# q1 = Session.query(Tag.id).outerjoin(MediaTag).filter(MediaTag.id==None)
|
||||
# q2 = Session.query(Tag).filter(Tag.id.in_(q1))
|
||||
# q2.delete(synchronize_session = False)
|
||||
|
||||
Session.commit()
|
||||
|
||||
|
||||
def check_collection_slug_used(dummy_db, creator_id, slug, ignore_c_id):
|
||||
filt = (Collection.creator == creator_id) \
|
||||
& (Collection.slug == slug)
|
||||
if ignore_c_id is not None:
|
||||
filt = filt & (Collection.id != ignore_c_id)
|
||||
does_exist = Session.query(Collection.id).filter(filt).first() is not None
|
||||
return does_exist
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from mediagoblin.db.sql.open import setup_connection_and_db_from_config
|
||||
|
||||
conn,db = setup_connection_and_db_from_config({'sql_engine':'sqlite:///mediagoblin.db'})
|
||||
|
||||
clean_orphan_tags()
|
||||
def inspect_table(metadata, table_name):
|
||||
"""Simple helper to get a ref to an already existing table"""
|
||||
return Table(table_name, metadata, autoload=True,
|
||||
autoload_with=metadata.bind)
|
289
mediagoblin/db/migrations.py
Normal file
289
mediagoblin/db/migrations.py
Normal file
@ -0,0 +1,289 @@
|
||||
# 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 datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
|
||||
Integer, Unicode, UnicodeText, DateTime,
|
||||
ForeignKey)
|
||||
from sqlalchemy.exc import ProgrammingError
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.sql import and_
|
||||
from migrate.changeset.constraint import UniqueConstraint
|
||||
|
||||
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
|
||||
from mediagoblin.db.models import MediaEntry, Collection, User
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
@RegisterMigration(1, MIGRATIONS)
|
||||
def ogg_to_webm_audio(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
file_keynames = Table('core__file_keynames', metadata, autoload=True,
|
||||
autoload_with=db_conn.bind)
|
||||
|
||||
db_conn.execute(
|
||||
file_keynames.update().where(file_keynames.c.name == 'ogg').
|
||||
values(name='webm_audio')
|
||||
)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(2, MIGRATIONS)
|
||||
def add_wants_notification_column(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
users = Table('core__users', metadata, autoload=True,
|
||||
autoload_with=db_conn.bind)
|
||||
|
||||
col = Column('wants_comment_notification', Boolean,
|
||||
default=True, nullable=True)
|
||||
col.create(users, populate_defaults=True)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(3, MIGRATIONS)
|
||||
def add_transcoding_progress(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
media_entry = inspect_table(metadata, 'core__media_entries')
|
||||
|
||||
col = Column('transcoding_progress', SmallInteger)
|
||||
col.create(media_entry)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
class Collection_v0(declarative_base()):
|
||||
__tablename__ = "core__collections"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(Unicode, nullable=False)
|
||||
slug = Column(Unicode)
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now,
|
||||
index=True)
|
||||
description = Column(UnicodeText)
|
||||
creator = Column(Integer, ForeignKey(User.id), nullable=False)
|
||||
items = Column(Integer, default=0)
|
||||
|
||||
class CollectionItem_v0(declarative_base()):
|
||||
__tablename__ = "core__collection_items"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
media_entry = Column(
|
||||
Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
|
||||
collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
|
||||
note = Column(UnicodeText, nullable=True)
|
||||
added = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
position = Column(Integer)
|
||||
|
||||
## This should be activated, normally.
|
||||
## But this would change the way the next migration used to work.
|
||||
## So it's commented for now.
|
||||
__table_args__ = (
|
||||
UniqueConstraint('collection', 'media_entry'),
|
||||
{})
|
||||
|
||||
collectionitem_unique_constraint_done = False
|
||||
|
||||
@RegisterMigration(4, MIGRATIONS)
|
||||
def add_collection_tables(db_conn):
|
||||
Collection_v0.__table__.create(db_conn.bind)
|
||||
CollectionItem_v0.__table__.create(db_conn.bind)
|
||||
|
||||
global collectionitem_unique_constraint_done
|
||||
collectionitem_unique_constraint_done = True
|
||||
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(5, MIGRATIONS)
|
||||
def add_mediaentry_collected(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
media_entry = inspect_table(metadata, 'core__media_entries')
|
||||
|
||||
col = Column('collected', Integer, default=0)
|
||||
col.create(media_entry)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
class ProcessingMetaData_v0(declarative_base()):
|
||||
__tablename__ = 'core__processing_metadata'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
|
||||
index=True)
|
||||
callback_url = Column(Unicode)
|
||||
|
||||
@RegisterMigration(6, MIGRATIONS)
|
||||
def create_processing_metadata_table(db):
|
||||
ProcessingMetaData_v0.__table__.create(db.bind)
|
||||
db.commit()
|
||||
|
||||
|
||||
# Okay, problem being:
|
||||
# Migration #4 forgot to add the uniqueconstraint for the
|
||||
# new tables. While creating the tables from scratch had
|
||||
# the constraint enabled.
|
||||
#
|
||||
# So we have four situations that should end up at the same
|
||||
# db layout:
|
||||
#
|
||||
# 1. Fresh install.
|
||||
# Well, easy. Just uses the tables in models.py
|
||||
# 2. Fresh install using a git version just before this migration
|
||||
# The tables are all there, the unique constraint is also there.
|
||||
# This migration should do nothing.
|
||||
# But as we can't detect the uniqueconstraint easily,
|
||||
# this migration just adds the constraint again.
|
||||
# And possibly fails very loud. But ignores the failure.
|
||||
# 3. old install, not using git, just releases.
|
||||
# This one will get the new tables in #4 (now with constraint!)
|
||||
# And this migration is just skipped silently.
|
||||
# 4. old install, always on latest git.
|
||||
# This one has the tables, but lacks the constraint.
|
||||
# So this migration adds the constraint.
|
||||
@RegisterMigration(7, MIGRATIONS)
|
||||
def fix_CollectionItem_v0_constraint(db_conn):
|
||||
"""Add the forgotten Constraint on CollectionItem"""
|
||||
|
||||
global collectionitem_unique_constraint_done
|
||||
if collectionitem_unique_constraint_done:
|
||||
# Reset it. Maybe the whole thing gets run again
|
||||
# For a different db?
|
||||
collectionitem_unique_constraint_done = False
|
||||
return
|
||||
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
CollectionItem_table = inspect_table(metadata, 'core__collection_items')
|
||||
|
||||
constraint = UniqueConstraint('collection', 'media_entry',
|
||||
name='core__collection_items_collection_media_entry_key',
|
||||
table=CollectionItem_table)
|
||||
|
||||
try:
|
||||
constraint.create()
|
||||
except ProgrammingError:
|
||||
# User probably has an install that was run since the
|
||||
# collection tables were added, so we don't need to run this migration.
|
||||
pass
|
||||
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(8, MIGRATIONS)
|
||||
def add_license_preference(db):
|
||||
metadata = MetaData(bind=db.bind)
|
||||
|
||||
user_table = inspect_table(metadata, 'core__users')
|
||||
|
||||
col = Column('license_preference', Unicode)
|
||||
col.create(user_table)
|
||||
db.commit()
|
||||
|
||||
|
||||
@RegisterMigration(9, MIGRATIONS)
|
||||
def mediaentry_new_slug_era(db):
|
||||
"""
|
||||
Update for the new era for media type slugs.
|
||||
|
||||
Entries without slugs now display differently in the url like:
|
||||
/u/cwebber/m/id=251/
|
||||
|
||||
... because of this, we should back-convert:
|
||||
- entries without slugs should be converted to use the id, if possible, to
|
||||
make old urls still work
|
||||
- slugs with = (or also : which is now also not allowed) to have those
|
||||
stripped out (small possibility of breakage here sadly)
|
||||
"""
|
||||
|
||||
def slug_and_user_combo_exists(slug, uploader):
|
||||
return db.execute(
|
||||
media_table.select(
|
||||
and_(media_table.c.uploader==uploader,
|
||||
media_table.c.slug==slug))).first() is not None
|
||||
|
||||
def append_garbage_till_unique(row, new_slug):
|
||||
"""
|
||||
Attach junk to this row until it's unique, then save it
|
||||
"""
|
||||
if slug_and_user_combo_exists(new_slug, row.uploader):
|
||||
# okay, still no success;
|
||||
# let's whack junk on there till it's unique.
|
||||
new_slug += '-' + uuid.uuid4().hex[:4]
|
||||
# keep going if necessary!
|
||||
while slug_and_user_combo_exists(new_slug, row.uploader):
|
||||
new_slug += uuid.uuid4().hex[:4]
|
||||
|
||||
db.execute(
|
||||
media_table.update(). \
|
||||
where(media_table.c.id==row.id). \
|
||||
values(slug=new_slug))
|
||||
|
||||
metadata = MetaData(bind=db.bind)
|
||||
|
||||
media_table = inspect_table(metadata, 'core__media_entries')
|
||||
|
||||
for row in db.execute(media_table.select()):
|
||||
# no slug, try setting to an id
|
||||
if not row.slug:
|
||||
append_garbage_till_unique(row, unicode(row.id))
|
||||
# has "=" or ":" in it... we're getting rid of those
|
||||
elif u"=" in row.slug or u":" in row.slug:
|
||||
append_garbage_till_unique(
|
||||
row, row.slug.replace(u"=", u"-").replace(u":", u"-"))
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@RegisterMigration(10, MIGRATIONS)
|
||||
def unique_collections_slug(db):
|
||||
"""Add unique constraint to collection slug"""
|
||||
metadata = MetaData(bind=db.bind)
|
||||
collection_table = inspect_table(metadata, "core__collections")
|
||||
existing_slugs = {}
|
||||
slugs_to_change = []
|
||||
|
||||
for row in db.execute(collection_table.select()):
|
||||
# if duplicate slug, generate a unique slug
|
||||
if row.creator in existing_slugs and row.slug in \
|
||||
existing_slugs[row.creator]:
|
||||
slugs_to_change.append(row.id)
|
||||
else:
|
||||
if not row.creator in existing_slugs:
|
||||
existing_slugs[row.creator] = [row.slug]
|
||||
else:
|
||||
existing_slugs[row.creator].append(row.slug)
|
||||
|
||||
for row_id in slugs_to_change:
|
||||
new_slug = unicode(uuid.uuid4())
|
||||
db.execute(collection_table.update().
|
||||
where(collection_table.c.id == row_id).
|
||||
values(slug=new_slug))
|
||||
# sqlite does not like to change the schema when a transaction(update) is
|
||||
# not yet completed
|
||||
db.commit()
|
||||
|
||||
constraint = UniqueConstraint('creator', 'slug',
|
||||
name='core__collection_creator_slug_key',
|
||||
table=collection_table)
|
||||
constraint.create()
|
||||
|
||||
db.commit()
|
@ -27,8 +27,13 @@ These functions now live here and get "mixed in" into the
|
||||
real objects.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.auth import lib as auth_lib
|
||||
from mediagoblin.media_types import get_media_managers, FileTypeNotSupported
|
||||
from mediagoblin.tools import common, licenses
|
||||
from mediagoblin.tools.text import cleaned_markdown_conversion
|
||||
from mediagoblin.tools.url import slugify
|
||||
@ -47,22 +52,83 @@ class UserMixin(object):
|
||||
return cleaned_markdown_conversion(self.bio)
|
||||
|
||||
|
||||
class MediaEntryMixin(object):
|
||||
class GenerateSlugMixin(object):
|
||||
"""
|
||||
Mixin to add a generate_slug method to objects.
|
||||
|
||||
Depends on:
|
||||
- self.slug
|
||||
- self.title
|
||||
- self.check_slug_used(new_slug)
|
||||
"""
|
||||
def generate_slug(self):
|
||||
"""
|
||||
Generate a unique slug for this object.
|
||||
|
||||
This one does not *force* slugs, but usually it will probably result
|
||||
in a niceish one.
|
||||
|
||||
The end *result* of the algorithm will result in these resolutions for
|
||||
these situations:
|
||||
- If we have a slug, make sure it's clean and sanitized, and if it's
|
||||
unique, we'll use that.
|
||||
- If we have a title, slugify it, and if it's unique, we'll use that.
|
||||
- If we can't get any sort of thing that looks like it'll be a useful
|
||||
slug out of a title or an existing slug, bail, and don't set the
|
||||
slug at all. Don't try to create something just because. Make
|
||||
sure we have a reasonable basis for a slug first.
|
||||
- If we have a reasonable basis for a slug (either based on existing
|
||||
slug or slugified title) but it's not unique, first try appending
|
||||
the entry's id, if that exists
|
||||
- If that doesn't result in something unique, tack on some randomly
|
||||
generated bits until it's unique. That'll be a little bit of junk,
|
||||
but at least it has the basis of a nice slug.
|
||||
"""
|
||||
#Is already a slug assigned? Check if it is valid
|
||||
if self.slug:
|
||||
self.slug = slugify(self.slug)
|
||||
|
||||
# otherwise, try to use the title.
|
||||
elif self.title:
|
||||
# assign slug based on title
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
# We don't want any empty string slugs
|
||||
if self.slug == u"":
|
||||
self.slug = None
|
||||
|
||||
# Do we have anything at this point?
|
||||
# If not, we're not going to get a slug
|
||||
# so just return... we're not going to force one.
|
||||
if not self.slug:
|
||||
return # giving up!
|
||||
|
||||
# Otherwise, let's see if this is unique.
|
||||
if self.check_slug_used(self.slug):
|
||||
# It looks like it's being used... lame.
|
||||
|
||||
# Can we just append the object's id to the end?
|
||||
if self.id:
|
||||
slug_with_id = u"%s-%s" % (self.slug, self.id)
|
||||
if not self.check_slug_used(slug_with_id):
|
||||
self.slug = slug_with_id
|
||||
return # success!
|
||||
|
||||
# okay, still no success;
|
||||
# let's whack junk on there till it's unique.
|
||||
self.slug += '-' + uuid.uuid4().hex[:4]
|
||||
# keep going if necessary!
|
||||
while self.check_slug_used(self.slug):
|
||||
self.slug += uuid.uuid4().hex[:4]
|
||||
|
||||
|
||||
class MediaEntryMixin(GenerateSlugMixin):
|
||||
def check_slug_used(self, slug):
|
||||
# import this here due to a cyclic import issue
|
||||
# (db.models -> db.mixin -> db.util -> db.models)
|
||||
from mediagoblin.db.util import check_media_slug_used
|
||||
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
duplicate = check_media_slug_used(mg_globals.database,
|
||||
self.uploader, self.slug, self.id)
|
||||
|
||||
if duplicate:
|
||||
if self.id is not None:
|
||||
self.slug = u"%s-%s" % (self.id, self.slug)
|
||||
else:
|
||||
self.slug = None
|
||||
return check_media_slug_used(self.uploader, slug, self.id)
|
||||
|
||||
@property
|
||||
def description_html(self):
|
||||
@ -72,37 +138,44 @@ class MediaEntryMixin(object):
|
||||
"""
|
||||
return cleaned_markdown_conversion(self.description)
|
||||
|
||||
def get_display_media(self, media_map,
|
||||
fetch_order=common.DISPLAY_IMAGE_FETCHING_ORDER):
|
||||
"""
|
||||
Find the best media for display.
|
||||
def get_display_media(self):
|
||||
"""Find the best media for display.
|
||||
|
||||
Args:
|
||||
- media_map: a dict like
|
||||
{u'image_size': [u'dir1', u'dir2', u'image.jpg']}
|
||||
- fetch_order: the order we should try fetching images in
|
||||
We try checking self.media_manager.fetching_order if it exists to
|
||||
pull down the order.
|
||||
|
||||
Returns:
|
||||
(media_size, media_path)
|
||||
"""
|
||||
media_sizes = media_map.keys()
|
||||
(media_size, media_path)
|
||||
or, if not found, None.
|
||||
|
||||
for media_size in common.DISPLAY_IMAGE_FETCHING_ORDER:
|
||||
"""
|
||||
fetch_order = self.media_manager.media_fetch_order
|
||||
|
||||
# No fetching order found? well, give up!
|
||||
if not fetch_order:
|
||||
return None
|
||||
|
||||
media_sizes = self.media_files.keys()
|
||||
|
||||
for media_size in fetch_order:
|
||||
if media_size in media_sizes:
|
||||
return media_map[media_size]
|
||||
return media_size, self.media_files[media_size]
|
||||
|
||||
def main_mediafile(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def slug_or_id(self):
|
||||
return (self.slug or self._id)
|
||||
if self.slug:
|
||||
return self.slug
|
||||
else:
|
||||
return u'id:%s' % self.id
|
||||
|
||||
def url_for_self(self, urlgen, **extra_args):
|
||||
"""
|
||||
Generate an appropriate url for ourselves
|
||||
|
||||
Use a slug if we have one, else use our '_id'.
|
||||
Use a slug if we have one, else use our 'id'.
|
||||
"""
|
||||
uploader = self.get_uploader
|
||||
|
||||
@ -112,6 +185,38 @@ class MediaEntryMixin(object):
|
||||
media=self.slug_or_id,
|
||||
**extra_args)
|
||||
|
||||
@property
|
||||
def thumb_url(self):
|
||||
"""Return the thumbnail URL (for usage in templates)
|
||||
Will return either the real thumbnail or a default fallback icon."""
|
||||
# TODO: implement generic fallback in case MEDIA_MANAGER does
|
||||
# not specify one?
|
||||
if u'thumb' in self.media_files:
|
||||
thumb_url = mg_globals.app.public_store.file_url(
|
||||
self.media_files[u'thumb'])
|
||||
else:
|
||||
# No thumbnail in media available. Get the media's
|
||||
# MEDIA_MANAGER for the fallback icon and return static URL
|
||||
# Raises FileTypeNotSupported in case no such manager is enabled
|
||||
manager = self.media_manager
|
||||
thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
|
||||
return thumb_url
|
||||
|
||||
@cached_property
|
||||
def media_manager(self):
|
||||
"""Returns the MEDIA_MANAGER of the media's media_type
|
||||
|
||||
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)
|
||||
# Not found? Then raise an error
|
||||
raise FileTypeNotSupported(
|
||||
"MediaManager not in enabled types. Check media_types in config?")
|
||||
|
||||
def get_fail_exception(self):
|
||||
"""
|
||||
Get the exception that's appropriate for this error
|
||||
@ -121,7 +226,7 @@ class MediaEntryMixin(object):
|
||||
|
||||
def get_license_data(self):
|
||||
"""Return license dict for requested license"""
|
||||
return licenses.SUPPORTED_LICENSES[self.license or ""]
|
||||
return licenses.get_license_by_url(self.license or "")
|
||||
|
||||
def exif_display_iter(self):
|
||||
from mediagoblin.tools.exif import USEFUL_TAGS
|
||||
@ -145,22 +250,13 @@ class MediaCommentMixin(object):
|
||||
return cleaned_markdown_conversion(self.content)
|
||||
|
||||
|
||||
class CollectionMixin(object):
|
||||
def generate_slug(self):
|
||||
class CollectionMixin(GenerateSlugMixin):
|
||||
def check_slug_used(self, slug):
|
||||
# import this here due to a cyclic import issue
|
||||
# (db.models -> db.mixin -> db.util -> db.models)
|
||||
from mediagoblin.db.util import check_collection_slug_used
|
||||
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
duplicate = check_collection_slug_used(mg_globals.database,
|
||||
self.creator, self.slug, self.id)
|
||||
|
||||
if duplicate:
|
||||
if self.id is not None:
|
||||
self.slug = u"%s-%s" % (self.id, self.slug)
|
||||
else:
|
||||
self.slug = None
|
||||
return check_collection_slug_used(self.creator, slug, self.id)
|
||||
|
||||
@property
|
||||
def description_html(self):
|
||||
@ -172,13 +268,13 @@ class CollectionMixin(object):
|
||||
|
||||
@property
|
||||
def slug_or_id(self):
|
||||
return (self.slug or self._id)
|
||||
return (self.slug or self.id)
|
||||
|
||||
def url_for_self(self, urlgen, **extra_args):
|
||||
"""
|
||||
Generate an appropriate url for ourselves
|
||||
|
||||
Use a slug if we have one, else use our '_id'.
|
||||
Use a slug if we have one, else use our 'id'.
|
||||
"""
|
||||
creator = self.get_creator
|
||||
|
||||
|
@ -18,9 +18,8 @@
|
||||
TODO: indexes on foreignkeys, where useful.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
||||
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
||||
@ -31,10 +30,11 @@ from sqlalchemy.sql.expression import desc
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.util import memoized_property
|
||||
|
||||
from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
|
||||
from mediagoblin.db.sql.base import Base, DictReadAttrProxy
|
||||
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.sql.base import Session
|
||||
from mediagoblin.tools.files import delete_media_files
|
||||
from mediagoblin.tools.common import import_component
|
||||
|
||||
# It's actually kind of annoying how sqlalchemy-migrate does this, if
|
||||
# I understand it right, but whatever. Anyway, don't remove this :P
|
||||
@ -43,17 +43,7 @@ from mediagoblin.db.sql.base import Session
|
||||
# this import-based meddling...
|
||||
from migrate import changeset
|
||||
|
||||
|
||||
class SimpleFieldAlias(object):
|
||||
"""An alias for any field"""
|
||||
def __init__(self, fieldname):
|
||||
self.fieldname = fieldname
|
||||
|
||||
def __get__(self, instance, cls):
|
||||
return getattr(instance, self.fieldname)
|
||||
|
||||
def __set__(self, instance, val):
|
||||
setattr(instance, self.fieldname, val)
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class User(Base, UserMixin):
|
||||
@ -65,6 +55,10 @@ class User(Base, UserMixin):
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(Unicode, nullable=False, unique=True)
|
||||
# Note: no db uniqueness constraint on email because it's not
|
||||
# reliable (many email systems case insensitive despite against
|
||||
# the RFC) and because it would be a mess to implement at this
|
||||
# point.
|
||||
email = Column(Unicode, nullable=False)
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
pw_hash = Column(Unicode, nullable=False)
|
||||
@ -73,6 +67,7 @@ class User(Base, UserMixin):
|
||||
# Intented to be nullable=False, but migrations would not work for it
|
||||
# set to nullable=True implicitly.
|
||||
wants_comment_notification = Column(Boolean, default=True)
|
||||
license_preference = Column(Unicode)
|
||||
verification_key = Column(Unicode)
|
||||
is_admin = Column(Boolean, default=False, nullable=False)
|
||||
url = Column(Unicode)
|
||||
@ -83,8 +78,6 @@ class User(Base, UserMixin):
|
||||
## TODO
|
||||
# plugin data would be in a separate model
|
||||
|
||||
_id = SimpleFieldAlias("id")
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} #{1} {2} {3} "{4}">'.format(
|
||||
self.__class__.__name__,
|
||||
@ -93,6 +86,25 @@ class User(Base, UserMixin):
|
||||
'admin' if self.is_admin else 'user',
|
||||
self.username)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
"""Deletes a User and all related entries/comments/files/..."""
|
||||
# Collections get deleted by relationships.
|
||||
|
||||
media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id)
|
||||
for media in media_entries:
|
||||
# TODO: Make sure that "MediaEntry.delete()" also deletes
|
||||
# all related files/Comments
|
||||
media.delete(del_orphan_tags=False, commit=False)
|
||||
|
||||
# Delete now unused tags
|
||||
# TODO: import here due to cyclic imports!!! This cries for refactoring
|
||||
from mediagoblin.db.util import clean_orphan_tags
|
||||
clean_orphan_tags(commit=False)
|
||||
|
||||
# Delete user, pass through commit=False/True in kwargs
|
||||
super(User, self).delete(**kwargs)
|
||||
_log.info('Deleted user "{0}" account'.format(self.username))
|
||||
|
||||
|
||||
class MediaEntry(Base, MediaEntryMixin):
|
||||
"""
|
||||
@ -146,7 +158,7 @@ class MediaEntry(Base, MediaEntryMixin):
|
||||
)
|
||||
|
||||
tags_helper = relationship("MediaTag",
|
||||
cascade="all, delete-orphan"
|
||||
cascade="all, delete-orphan" # should be automatically deleted
|
||||
)
|
||||
tags = association_proxy("tags_helper", "dict_view",
|
||||
creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
|
||||
@ -158,17 +170,13 @@ class MediaEntry(Base, MediaEntryMixin):
|
||||
collections = association_proxy("collections_helper", "in_collection")
|
||||
|
||||
## TODO
|
||||
# media_data
|
||||
# fail_error
|
||||
|
||||
_id = SimpleFieldAlias("id")
|
||||
|
||||
def get_comments(self, ascending=False):
|
||||
order_col = MediaComment.created
|
||||
if not ascending:
|
||||
order_col = desc(order_col)
|
||||
return MediaComment.query.filter_by(
|
||||
media_entry=self.id).order_by(order_col)
|
||||
return self.all_comments.order_by(order_col)
|
||||
|
||||
def url_to_prev(self, urlgen):
|
||||
"""get the next 'newer' entry by this user"""
|
||||
@ -190,40 +198,31 @@ class MediaEntry(Base, MediaEntryMixin):
|
||||
if media is not None:
|
||||
return media.url_for_self(urlgen)
|
||||
|
||||
#@memoized_property
|
||||
@property
|
||||
def media_data(self):
|
||||
session = Session()
|
||||
|
||||
return session.query(self.media_data_table).filter_by(
|
||||
media_entry=self.id).first()
|
||||
return getattr(self, self.media_data_ref)
|
||||
|
||||
def media_data_init(self, **kwargs):
|
||||
"""
|
||||
Initialize or update the contents of a media entry's media_data row
|
||||
"""
|
||||
session = Session()
|
||||
media_data = self.media_data
|
||||
|
||||
media_data = session.query(self.media_data_table).filter_by(
|
||||
media_entry=self.id).first()
|
||||
|
||||
# No media data, so actually add a new one
|
||||
if media_data is None:
|
||||
media_data = self.media_data_table(
|
||||
media_entry=self.id,
|
||||
**kwargs)
|
||||
session.add(media_data)
|
||||
# Update old media data
|
||||
# Get the correct table:
|
||||
table = import_component(self.media_type + '.models:DATA_MODEL')
|
||||
# No media data, so actually add a new one
|
||||
media_data = table(**kwargs)
|
||||
# Get the relationship set up.
|
||||
media_data.get_media_entry = self
|
||||
else:
|
||||
# Update old media data
|
||||
for field, value in kwargs.iteritems():
|
||||
setattr(media_data, field, value)
|
||||
|
||||
@memoized_property
|
||||
def media_data_table(self):
|
||||
# TODO: memoize this
|
||||
models_module = self.media_type + '.models'
|
||||
__import__(models_module)
|
||||
return sys.modules[models_module].DATA_MODEL
|
||||
def media_data_ref(self):
|
||||
return import_component(self.media_type + '.models:BACKREF_NAME')
|
||||
|
||||
def __repr__(self):
|
||||
safe_title = self.title.encode('ascii', 'replace')
|
||||
@ -233,6 +232,35 @@ class MediaEntry(Base, MediaEntryMixin):
|
||||
id=self.id,
|
||||
title=safe_title)
|
||||
|
||||
def delete(self, del_orphan_tags=True, **kwargs):
|
||||
"""Delete MediaEntry and all related files/attachments/comments
|
||||
|
||||
This will *not* automatically delete unused collections, which
|
||||
can remain empty...
|
||||
|
||||
: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.
|
||||
|
||||
# Delete all related files/attachments
|
||||
try:
|
||||
delete_media_files(self)
|
||||
except OSError, error:
|
||||
# Returns list of files we failed to delete
|
||||
_log.error('No such files from the user "{1}" to delete: '
|
||||
'{0}'.format(str(error), self.get_uploader))
|
||||
_log.info('Deleted Media entry id "{0}"'.format(self.id))
|
||||
# Related MediaTag's are automatically cleaned, but we might
|
||||
# want to clean out unused Tag's too.
|
||||
if del_orphan_tags:
|
||||
# TODO: Import here due to cyclic imports!!!
|
||||
# This cries for refactoring
|
||||
from mediagoblin.db.util import clean_orphan_tags
|
||||
clean_orphan_tags(commit=False)
|
||||
# pass through commit=False/True in kwargs
|
||||
super(MediaEntry, self).delete(**kwargs)
|
||||
|
||||
|
||||
class FileKeynames(Base):
|
||||
"""
|
||||
@ -357,34 +385,58 @@ class MediaComment(Base, MediaCommentMixin):
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
content = Column(UnicodeText, nullable=False)
|
||||
|
||||
get_author = relationship(User)
|
||||
# Cascade: Comments are owned by their creator. So do the full thing.
|
||||
# lazy=dynamic: People might post a *lot* of comments,
|
||||
# so make the "posted_comments" a query-like thing.
|
||||
get_author = relationship(User,
|
||||
backref=backref("posted_comments",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan"))
|
||||
|
||||
_id = SimpleFieldAlias("id")
|
||||
# Cascade: Comments are somewhat owned by their MediaEntry.
|
||||
# So do the full thing.
|
||||
# lazy=dynamic: MediaEntries might have many comments,
|
||||
# so make the "all_comments" a query-like thing.
|
||||
get_media_entry = relationship(MediaEntry,
|
||||
backref=backref("all_comments",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan"))
|
||||
|
||||
|
||||
class Collection(Base, CollectionMixin):
|
||||
"""An 'album' or 'set' of media by a user.
|
||||
|
||||
On deletion, contained CollectionItems get automatically reaped via
|
||||
SQL cascade"""
|
||||
__tablename__ = "core__collections"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(Unicode, nullable=False)
|
||||
slug = Column(Unicode)
|
||||
created = Column(DateTime, nullable=False, default=datetime.datetime.now,
|
||||
index=True)
|
||||
index=True)
|
||||
description = Column(UnicodeText)
|
||||
creator = Column(Integer, ForeignKey(User.id), nullable=False)
|
||||
# TODO: No of items in Collection. Badly named, can we migrate to num_items?
|
||||
items = Column(Integer, default=0)
|
||||
|
||||
get_creator = relationship(User)
|
||||
# Cascade: Collections are owned by their creator. So do the full thing.
|
||||
get_creator = relationship(User,
|
||||
backref=backref("collections",
|
||||
cascade="all, delete-orphan"))
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('creator', 'slug'),
|
||||
{})
|
||||
|
||||
def get_collection_items(self, ascending=False):
|
||||
#TODO, is this still needed with self.collection_items being available?
|
||||
order_col = CollectionItem.position
|
||||
if not ascending:
|
||||
order_col = desc(order_col)
|
||||
return CollectionItem.query.filter_by(
|
||||
collection=self.id).order_by(order_col)
|
||||
|
||||
_id = SimpleFieldAlias("id")
|
||||
|
||||
|
||||
class CollectionItem(Base, CollectionItemMixin):
|
||||
__tablename__ = "core__collection_items"
|
||||
@ -396,12 +448,15 @@ class CollectionItem(Base, CollectionItemMixin):
|
||||
note = Column(UnicodeText, nullable=True)
|
||||
added = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
position = Column(Integer)
|
||||
in_collection = relationship("Collection")
|
||||
|
||||
# Cascade: CollectionItems are owned by their Collection. So do the full thing.
|
||||
in_collection = relationship(Collection,
|
||||
backref=backref(
|
||||
"collection_items",
|
||||
cascade="all, delete-orphan"))
|
||||
|
||||
get_media_entry = relationship(MediaEntry)
|
||||
|
||||
_id = SimpleFieldAlias("id")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('collection', 'media_entry'),
|
||||
{})
|
@ -31,9 +31,8 @@ from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.util import memoized_property
|
||||
|
||||
from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
|
||||
from mediagoblin.db.sql.base import GMGTableBase
|
||||
from mediagoblin.db.sql.base import Session
|
||||
from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
|
||||
from mediagoblin.db.base import GMGTableBase, Session
|
||||
|
||||
|
||||
Base_v0 = declarative_base(cls=GMGTableBase)
|
@ -1,146 +0,0 @@
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Indexes for the local database.
|
||||
|
||||
To add new indexes
|
||||
------------------
|
||||
|
||||
Indexes are recorded in the following format:
|
||||
|
||||
ACTIVE_INDEXES = {
|
||||
'collection_name': {
|
||||
'identifier': { # key identifier used for possibly deprecating later
|
||||
'index': [index_foo_goes_here]}}
|
||||
|
||||
... and anything else being parameters to the create_index function
|
||||
(including unique=True, etc)
|
||||
|
||||
Current indexes must be registered in ACTIVE_INDEXES... deprecated
|
||||
indexes should be marked in DEPRECATED_INDEXES.
|
||||
|
||||
Remember, ordering of compound indexes MATTERS. Read below for more.
|
||||
|
||||
REQUIRED READING:
|
||||
- http://kylebanker.com/blog/2010/09/21/the-joy-of-mongodb-indexes/
|
||||
|
||||
- http://www.mongodb.org/display/DOCS/Indexes
|
||||
- http://www.mongodb.org/display/DOCS/Indexing+Advice+and+FAQ
|
||||
|
||||
|
||||
To remove deprecated indexes
|
||||
----------------------------
|
||||
|
||||
Removing deprecated indexes is the same, just move the index into the
|
||||
deprecated indexes mapping.
|
||||
|
||||
DEPRECATED_INDEXES = {
|
||||
'collection_name': {
|
||||
'deprecated_index_identifier1': {
|
||||
'index': [index_foo_goes_here]}}
|
||||
|
||||
... etc.
|
||||
|
||||
If an index has been deprecated that identifier should NEVER BE USED
|
||||
AGAIN. Eg, if you previously had 'awesomepants_unique', you shouldn't
|
||||
use 'awesomepants_unique' again, you should create a totally new name
|
||||
or at worst use 'awesomepants_unique2'.
|
||||
"""
|
||||
|
||||
from pymongo import ASCENDING, DESCENDING
|
||||
|
||||
|
||||
################
|
||||
# Active indexes
|
||||
################
|
||||
ACTIVE_INDEXES = {}
|
||||
|
||||
# MediaEntry indexes
|
||||
# ------------------
|
||||
|
||||
MEDIAENTRY_INDEXES = {
|
||||
'uploader_slug_unique': {
|
||||
# Matching an object to an uploader + slug.
|
||||
# MediaEntries are unique on these two combined, eg:
|
||||
# /u/${myuser}/m/${myslugname}/
|
||||
'index': [('uploader', ASCENDING),
|
||||
('slug', ASCENDING)],
|
||||
'unique': True},
|
||||
|
||||
'created': {
|
||||
# A global index for all media entries created, in descending
|
||||
# order. This is used for the site's frontpage.
|
||||
'index': [('created', DESCENDING)]},
|
||||
|
||||
'uploader_created': {
|
||||
# Indexing on uploaders and when media entries are created.
|
||||
# Used for showing a user gallery, etc.
|
||||
'index': [('uploader', ASCENDING),
|
||||
('created', DESCENDING)]},
|
||||
|
||||
'state_uploader_tags_created': {
|
||||
# Indexing on processed?, media uploader, associated tags, and
|
||||
# timestamp Used for showing media items matching a tag
|
||||
# search, most recent first.
|
||||
'index': [('state', ASCENDING),
|
||||
('uploader', ASCENDING),
|
||||
('tags.slug', DESCENDING),
|
||||
('created', DESCENDING)]},
|
||||
|
||||
'state_tags_created': {
|
||||
# Indexing on processed?, media tags, and timestamp (across all users)
|
||||
# This is used for a front page tag search.
|
||||
'index': [('state', ASCENDING),
|
||||
('tags.slug', DESCENDING),
|
||||
('created', DESCENDING)]}}
|
||||
|
||||
|
||||
ACTIVE_INDEXES['media_entries'] = MEDIAENTRY_INDEXES
|
||||
|
||||
|
||||
# User indexes
|
||||
# ------------
|
||||
|
||||
USER_INDEXES = {
|
||||
'username_unique': {
|
||||
# Index usernames, and make sure they're unique.
|
||||
# ... I guess we might need to adjust this once we're federated :)
|
||||
'index': 'username',
|
||||
'unique': True},
|
||||
'created': {
|
||||
# All most recently created users
|
||||
'index': 'created'}}
|
||||
|
||||
|
||||
ACTIVE_INDEXES['users'] = USER_INDEXES
|
||||
|
||||
|
||||
# MediaComment indexes
|
||||
|
||||
MEDIA_COMMENT_INDEXES = {
|
||||
'mediaentry_created': {
|
||||
'index': [('media_entry', ASCENDING),
|
||||
('created', DESCENDING)]}}
|
||||
|
||||
ACTIVE_INDEXES['media_comments'] = MEDIA_COMMENT_INDEXES
|
||||
|
||||
|
||||
####################
|
||||
# Deprecated indexes
|
||||
####################
|
||||
|
||||
DEPRECATED_INDEXES = {}
|
@ -1,208 +0,0 @@
|
||||
# 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.db.mongo.util import RegisterMigration
|
||||
from mediagoblin.tools.text import cleaned_markdown_conversion
|
||||
|
||||
|
||||
def add_table_field(db, table_name, field_name, default_value):
|
||||
"""
|
||||
Add a new field to the table/collection named table_name.
|
||||
The field will have the name field_name and the value default_value
|
||||
"""
|
||||
db[table_name].update(
|
||||
{field_name: {'$exists': False}},
|
||||
{'$set': {field_name: default_value}},
|
||||
multi=True)
|
||||
|
||||
|
||||
def drop_table_field(db, table_name, field_name):
|
||||
"""
|
||||
Drop an old field from a table/collection
|
||||
"""
|
||||
db[table_name].update(
|
||||
{field_name: {'$exists': True}},
|
||||
{'$unset': {field_name: 1}},
|
||||
multi=True)
|
||||
|
||||
|
||||
# Please see mediagoblin/tests/test_migrations.py for some examples of
|
||||
# basic migrations.
|
||||
|
||||
|
||||
@RegisterMigration(1)
|
||||
def user_add_bio_html(database):
|
||||
"""
|
||||
Users now have richtext bios via Markdown, reflect appropriately.
|
||||
"""
|
||||
collection = database['users']
|
||||
|
||||
target = collection.find(
|
||||
{'bio_html': {'$exists': False}})
|
||||
|
||||
for document in target:
|
||||
document['bio_html'] = cleaned_markdown_conversion(
|
||||
document['bio'])
|
||||
collection.save(document)
|
||||
|
||||
|
||||
@RegisterMigration(2)
|
||||
def mediaentry_mediafiles_main_to_original(database):
|
||||
"""
|
||||
Rename "main" media file to "original".
|
||||
"""
|
||||
collection = database['media_entries']
|
||||
target = collection.find(
|
||||
{'media_files.main': {'$exists': True}})
|
||||
|
||||
for document in target:
|
||||
original = document['media_files'].pop('main')
|
||||
document['media_files']['original'] = original
|
||||
|
||||
collection.save(document)
|
||||
|
||||
|
||||
@RegisterMigration(3)
|
||||
def mediaentry_remove_thumbnail_file(database):
|
||||
"""
|
||||
Use media_files['thumb'] instead of media_entries['thumbnail_file']
|
||||
"""
|
||||
database['media_entries'].update(
|
||||
{'thumbnail_file': {'$exists': True}},
|
||||
{'$unset': {'thumbnail_file': 1}},
|
||||
multi=True)
|
||||
|
||||
|
||||
@RegisterMigration(4)
|
||||
def mediaentry_add_queued_task_id(database):
|
||||
"""
|
||||
Add the 'queued_task_id' field for entries that don't have it.
|
||||
"""
|
||||
add_table_field(database, 'media_entries', 'queued_task_id', None)
|
||||
|
||||
|
||||
@RegisterMigration(5)
|
||||
def mediaentry_add_fail_error_and_metadata(database):
|
||||
"""
|
||||
Add 'fail_error' and 'fail_metadata' fields to media entries
|
||||
"""
|
||||
add_table_field(database, 'media_entries', 'fail_error', None)
|
||||
add_table_field(database, 'media_entries', 'fail_metadata', {})
|
||||
|
||||
|
||||
@RegisterMigration(6)
|
||||
def user_add_forgot_password_token_and_expires(database):
|
||||
"""
|
||||
Add token and expiration fields to help recover forgotten passwords
|
||||
"""
|
||||
add_table_field(database, 'users', 'fp_verification_key', None)
|
||||
add_table_field(database, 'users', 'fp_token_expire', None)
|
||||
|
||||
|
||||
@RegisterMigration(7)
|
||||
def media_type_image_to_multimedia_type_image(database):
|
||||
database['media_entries'].update(
|
||||
{'media_type': 'image'},
|
||||
{'$set': {'media_type': 'mediagoblin.media_types.image'}},
|
||||
multi=True)
|
||||
|
||||
|
||||
@RegisterMigration(8)
|
||||
def mediaentry_add_license(database):
|
||||
"""
|
||||
Add the 'license' field for entries that don't have it.
|
||||
"""
|
||||
add_table_field(database, 'media_entries', 'license', None)
|
||||
|
||||
|
||||
@RegisterMigration(9)
|
||||
def remove_calculated_html(database):
|
||||
"""
|
||||
Drop pre-rendered html again and calculate things
|
||||
on the fly (and cache):
|
||||
- User.bio_html
|
||||
- MediaEntry.description_html
|
||||
- MediaComment.content_html
|
||||
"""
|
||||
drop_table_field(database, 'users', 'bio_html')
|
||||
drop_table_field(database, 'media_entries', 'description_html')
|
||||
drop_table_field(database, 'media_comments', 'content_html')
|
||||
|
||||
|
||||
@RegisterMigration(10)
|
||||
def convert_video_media_data(database):
|
||||
"""
|
||||
Move media_data["video"] directly into media_data
|
||||
"""
|
||||
collection = database['media_entries']
|
||||
target = collection.find(
|
||||
{'media_data.video': {'$exists': True}})
|
||||
|
||||
for document in target:
|
||||
assert len(document['media_data']) == 1
|
||||
document['media_data'] = document['media_data']['video']
|
||||
collection.save(document)
|
||||
|
||||
|
||||
@RegisterMigration(11)
|
||||
def convert_gps_media_data(database):
|
||||
"""
|
||||
Move media_data["gps"]["*"] to media_data["gps_*"].
|
||||
In preparation for media_data.gps_*
|
||||
"""
|
||||
collection = database['media_entries']
|
||||
target = collection.find(
|
||||
{'media_data.gps': {'$exists': True}})
|
||||
|
||||
for document in target:
|
||||
for key, value in document['media_data']['gps'].iteritems():
|
||||
document['media_data']['gps_' + key] = value
|
||||
del document['media_data']['gps']
|
||||
collection.save(document)
|
||||
|
||||
|
||||
@RegisterMigration(12)
|
||||
def convert_exif_media_data(database):
|
||||
"""
|
||||
Move media_data["exif"]["clean"] to media_data["exif_all"].
|
||||
Drop media_data["exif"]["useful"]
|
||||
In preparation for media_data.exif_all
|
||||
"""
|
||||
collection = database['media_entries']
|
||||
target = collection.find(
|
||||
{'media_data.exif.clean': {'$exists': True}})
|
||||
|
||||
for document in target:
|
||||
media_data = document['media_data']
|
||||
|
||||
exif_all = media_data['exif'].pop('clean')
|
||||
if len(exif_all):
|
||||
media_data['exif_all'] = exif_all
|
||||
|
||||
del media_data['exif']['useful']
|
||||
|
||||
assert len(media_data['exif']) == 0
|
||||
del media_data['exif']
|
||||
|
||||
collection.save(document)
|
||||
|
||||
|
||||
@RegisterMigration(13)
|
||||
def user_add_wants_comment_notification(database):
|
||||
"""
|
||||
Add wants_comment_notification to user model
|
||||
"""
|
||||
add_table_field(database, 'users', 'wants_comment_notification', True)
|
@ -1,310 +0,0 @@
|
||||
# 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 datetime
|
||||
|
||||
from mongokit import Document
|
||||
|
||||
from mediagoblin.db.mongo import migrations
|
||||
from mediagoblin.db.mongo.util import ASCENDING, DESCENDING, ObjectId
|
||||
from mediagoblin.tools.pagination import Pagination
|
||||
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
|
||||
|
||||
|
||||
class MongoPK(object):
|
||||
"""An alias for the _id primary key"""
|
||||
def __get__(self, instance, cls):
|
||||
return instance['_id']
|
||||
def __set__(self, instance, val):
|
||||
instance['_id'] = val
|
||||
def __delete__(self, instance):
|
||||
del instance['_id']
|
||||
|
||||
|
||||
###################
|
||||
# Custom validators
|
||||
###################
|
||||
|
||||
########
|
||||
# Models
|
||||
########
|
||||
|
||||
|
||||
class User(Document, UserMixin):
|
||||
"""
|
||||
A user of MediaGoblin.
|
||||
|
||||
Structure:
|
||||
- username: The username of this user, should be unique to this instance.
|
||||
- email: Email address of this user
|
||||
- created: When the user was created
|
||||
- plugin_data: a mapping of extra plugin information for this User.
|
||||
Nothing uses this yet as we don't have plugins, but someday we
|
||||
might... :)
|
||||
- pw_hash: Hashed version of user's password.
|
||||
- email_verified: Whether or not the user has verified their email or not.
|
||||
Most parts of the site are disabled for users who haven't yet.
|
||||
- status: whether or not the user is active, etc. Currently only has two
|
||||
values, 'needs_email_verification' or 'active'. (In the future, maybe
|
||||
we'll change this to a boolean with a key of 'active' and have a
|
||||
separate field for a reason the user's been disabled if that's
|
||||
appropriate... email_verified is already separate, after all.)
|
||||
- wants_comment_notification: The user has selected that they want to be
|
||||
notified when comments are posted on their media.
|
||||
- verification_key: If the user is awaiting email verification, the user
|
||||
will have to provide this key (which will be encoded in the presented
|
||||
URL) in order to confirm their email as active.
|
||||
- is_admin: Whether or not this user is an administrator or not.
|
||||
- url: this user's personal webpage/website, if appropriate.
|
||||
- bio: biography of this user (plaintext, in markdown)
|
||||
"""
|
||||
__collection__ = 'users'
|
||||
use_dot_notation = True
|
||||
|
||||
structure = {
|
||||
'username': unicode,
|
||||
'email': unicode,
|
||||
'created': datetime.datetime,
|
||||
'plugin_data': dict, # plugins can dump stuff here.
|
||||
'pw_hash': unicode,
|
||||
'email_verified': bool,
|
||||
'status': unicode,
|
||||
'wants_comment_notification': bool,
|
||||
'verification_key': unicode,
|
||||
'is_admin': bool,
|
||||
'url': unicode,
|
||||
'bio': unicode, # May contain markdown
|
||||
'fp_verification_key': unicode, # forgotten password verification key
|
||||
'fp_token_expire': datetime.datetime,
|
||||
}
|
||||
|
||||
required_fields = ['username', 'created', 'pw_hash', 'email']
|
||||
|
||||
default_values = {
|
||||
'created': datetime.datetime.utcnow,
|
||||
'email_verified': False,
|
||||
'wants_comment_notification': True,
|
||||
'status': u'needs_email_verification',
|
||||
'is_admin': False}
|
||||
|
||||
id = MongoPK()
|
||||
|
||||
|
||||
class MediaEntry(Document, MediaEntryMixin):
|
||||
"""
|
||||
Record of a piece of media.
|
||||
|
||||
Structure:
|
||||
- uploader: A reference to a User who uploaded this.
|
||||
|
||||
- title: Title of this work
|
||||
|
||||
- slug: A normalized "slug" which can be used as part of a URL to retrieve
|
||||
this work, such as 'my-works-name-in-slug-form' may be viewable by
|
||||
'http://mg.example.org/u/username/m/my-works-name-in-slug-form/'
|
||||
Note that since URLs are constructed this way, slugs must be unique
|
||||
per-uploader. (An index is provided to enforce that but code should be
|
||||
written on the python side to ensure this as well.)
|
||||
|
||||
- created: Date and time of when this piece of work was uploaded.
|
||||
|
||||
- description: Uploader-set description of this work. This can be marked
|
||||
up with MarkDown for slight fanciness (links, boldness, italics,
|
||||
paragraphs...)
|
||||
|
||||
- media_type: What type of media is this? Currently we only support
|
||||
'image' ;)
|
||||
|
||||
- media_data: Extra information that's media-format-dependent.
|
||||
For example, images might contain some EXIF data that's not appropriate
|
||||
to other formats. You might store it like:
|
||||
|
||||
mediaentry.media_data['exif'] = {
|
||||
'manufacturer': 'CASIO',
|
||||
'model': 'QV-4000',
|
||||
'exposure_time': .659}
|
||||
|
||||
Alternately for video you might store:
|
||||
|
||||
# play length in seconds
|
||||
mediaentry.media_data['play_length'] = 340
|
||||
|
||||
... so what's appropriate here really depends on the media type.
|
||||
|
||||
- plugin_data: a mapping of extra plugin information for this User.
|
||||
Nothing uses this yet as we don't have plugins, but someday we
|
||||
might... :)
|
||||
|
||||
- tags: A list of tags. Each tag is stored as a dictionary that has a key
|
||||
for the actual name and the normalized name-as-slug, so ultimately this
|
||||
looks like:
|
||||
[{'name': 'Gully Gardens',
|
||||
'slug': 'gully-gardens'},
|
||||
{'name': 'Castle Adventure Time?!",
|
||||
'slug': 'castle-adventure-time'}]
|
||||
|
||||
- state: What's the state of this file? Active, inactive, disabled, etc...
|
||||
But really for now there are only two states:
|
||||
"unprocessed": uploaded but needs to go through processing for display
|
||||
"processed": processed and able to be displayed
|
||||
|
||||
- license: URI for media's license.
|
||||
|
||||
- queued_media_file: storage interface style filepath describing a file
|
||||
queued for processing. This is stored in the mg_globals.queue_store
|
||||
storage system.
|
||||
|
||||
- queued_task_id: celery task id. Use this to fetch the task state.
|
||||
|
||||
- media_files: Files relevant to this that have actually been processed
|
||||
and are available for various types of display. Stored like:
|
||||
{'thumb': ['dir1', 'dir2', 'pic.png'}
|
||||
|
||||
- attachment_files: A list of "attachment" files, ones that aren't
|
||||
critical to this piece of media but may be usefully relevant to people
|
||||
viewing the work. (currently unused.)
|
||||
|
||||
- fail_error: path to the exception raised
|
||||
- fail_metadata:
|
||||
"""
|
||||
__collection__ = 'media_entries'
|
||||
use_dot_notation = True
|
||||
|
||||
structure = {
|
||||
'uploader': ObjectId,
|
||||
'title': unicode,
|
||||
'slug': unicode,
|
||||
'created': datetime.datetime,
|
||||
'description': unicode, # May contain markdown/up
|
||||
'media_type': unicode,
|
||||
'media_data': dict, # extra data relevant to this media_type
|
||||
'plugin_data': dict, # plugins can dump stuff here.
|
||||
'tags': [dict],
|
||||
'state': unicode,
|
||||
'license': unicode,
|
||||
|
||||
# For now let's assume there can only be one main file queued
|
||||
# at a time
|
||||
'queued_media_file': [unicode],
|
||||
'queued_task_id': unicode,
|
||||
|
||||
# A dictionary of logical names to filepaths
|
||||
'media_files': dict,
|
||||
|
||||
# The following should be lists of lists, in appropriate file
|
||||
# record form
|
||||
'attachment_files': list,
|
||||
|
||||
# If things go badly in processing things, we'll store that
|
||||
# data here
|
||||
'fail_error': unicode,
|
||||
'fail_metadata': dict}
|
||||
|
||||
required_fields = [
|
||||
'uploader', 'created', 'media_type', 'slug']
|
||||
|
||||
default_values = {
|
||||
'created': datetime.datetime.utcnow,
|
||||
'state': u'unprocessed'}
|
||||
|
||||
id = MongoPK()
|
||||
|
||||
def media_data_init(self, **kwargs):
|
||||
self.media_data.update(kwargs)
|
||||
|
||||
def get_comments(self, ascending=False):
|
||||
if ascending:
|
||||
order = ASCENDING
|
||||
else:
|
||||
order = DESCENDING
|
||||
|
||||
return self.db.MediaComment.find({
|
||||
'media_entry': self._id}).sort('created', order)
|
||||
|
||||
def url_to_prev(self, urlgen):
|
||||
"""
|
||||
Provide a url to the previous entry from this user, if there is one
|
||||
"""
|
||||
cursor = self.db.MediaEntry.find({'_id': {"$gt": self._id},
|
||||
'uploader': self.uploader,
|
||||
'state': 'processed'}).sort(
|
||||
'_id', ASCENDING).limit(1)
|
||||
for media in cursor:
|
||||
return media.url_for_self(urlgen)
|
||||
|
||||
def url_to_next(self, urlgen):
|
||||
"""
|
||||
Provide a url to the next entry from this user, if there is one
|
||||
"""
|
||||
cursor = self.db.MediaEntry.find({'_id': {"$lt": self._id},
|
||||
'uploader': self.uploader,
|
||||
'state': 'processed'}).sort(
|
||||
'_id', DESCENDING).limit(1)
|
||||
|
||||
for media in cursor:
|
||||
return media.url_for_self(urlgen)
|
||||
|
||||
@property
|
||||
def get_uploader(self):
|
||||
return self.db.User.find_one({'_id': self.uploader})
|
||||
|
||||
|
||||
class MediaComment(Document, MediaCommentMixin):
|
||||
"""
|
||||
A comment on a MediaEntry.
|
||||
|
||||
Structure:
|
||||
- media_entry: The media entry this comment is attached to
|
||||
- author: user who posted this comment
|
||||
- created: when the comment was created
|
||||
- content: plaintext (but markdown'able) version of the comment's content.
|
||||
"""
|
||||
|
||||
__collection__ = 'media_comments'
|
||||
use_dot_notation = True
|
||||
|
||||
structure = {
|
||||
'media_entry': ObjectId,
|
||||
'author': ObjectId,
|
||||
'created': datetime.datetime,
|
||||
'content': unicode,
|
||||
}
|
||||
|
||||
required_fields = [
|
||||
'media_entry', 'author', 'created', 'content']
|
||||
|
||||
default_values = {
|
||||
'created': datetime.datetime.utcnow}
|
||||
|
||||
def media_entry(self):
|
||||
return self.db.MediaEntry.find_one({'_id': self['media_entry']})
|
||||
|
||||
@property
|
||||
def get_author(self):
|
||||
return self.db.User.find_one({'_id': self['author']})
|
||||
|
||||
|
||||
REGISTER_MODELS = [
|
||||
MediaEntry,
|
||||
User,
|
||||
MediaComment]
|
||||
|
||||
|
||||
def register_models(connection):
|
||||
"""
|
||||
Register all models in REGISTER_MODELS with this connection.
|
||||
"""
|
||||
connection.register(REGISTER_MODELS)
|
@ -1,82 +0,0 @@
|
||||
# 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 pymongo
|
||||
import mongokit
|
||||
from paste.deploy.converters import asint
|
||||
from mediagoblin.db.mongo import models
|
||||
from mediagoblin.db.mongo.util import MigrationManager
|
||||
|
||||
|
||||
def load_models(app_config):
|
||||
pass
|
||||
|
||||
|
||||
def connect_database_from_config(app_config, use_pymongo=False):
|
||||
"""
|
||||
Connect to the main database, take config from app_config
|
||||
|
||||
Optionally use pymongo instead of mongokit for the connection.
|
||||
"""
|
||||
port = app_config.get('db_port')
|
||||
if port:
|
||||
port = asint(port)
|
||||
|
||||
if use_pymongo:
|
||||
connection = pymongo.Connection(
|
||||
app_config.get('db_host'), port)
|
||||
else:
|
||||
connection = mongokit.Connection(
|
||||
app_config.get('db_host'), port)
|
||||
return connection
|
||||
|
||||
|
||||
def setup_connection_and_db_from_config(app_config, use_pymongo=False):
|
||||
"""
|
||||
Setup connection and database from config.
|
||||
|
||||
Optionally use pymongo instead of mongokit.
|
||||
"""
|
||||
connection = connect_database_from_config(app_config, use_pymongo)
|
||||
database_path = app_config['db_name']
|
||||
db = connection[database_path]
|
||||
|
||||
if not use_pymongo:
|
||||
models.register_models(connection)
|
||||
|
||||
return (connection, db)
|
||||
|
||||
|
||||
def check_db_migrations_current(db):
|
||||
# This MUST be imported so as to set up the appropriate migrations!
|
||||
from mediagoblin.db.mongo import migrations
|
||||
|
||||
# Init the migration number if necessary
|
||||
migration_manager = MigrationManager(db)
|
||||
migration_manager.install_migration_version_if_missing()
|
||||
|
||||
# Tiny hack to warn user if our migration is out of date
|
||||
if not migration_manager.database_at_latest_migration():
|
||||
db_migration_num = migration_manager.database_current_migration()
|
||||
latest_migration_num = migration_manager.latest_migration()
|
||||
if db_migration_num < latest_migration_num:
|
||||
print (
|
||||
"*WARNING:* Your migrations are out of date, "
|
||||
"maybe run ./bin/gmg migrate?")
|
||||
elif db_migration_num > latest_migration_num:
|
||||
print (
|
||||
"*WARNING:* Your migrations are out of date... "
|
||||
"in fact they appear to be from the future?!")
|
@ -1,318 +0,0 @@
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Utilities for database operations.
|
||||
|
||||
Some note on migration and indexing tools:
|
||||
|
||||
We store information about what the state of the database is in the
|
||||
'mediagoblin' document of the 'app_metadata' collection. Keys in that
|
||||
document relevant to here:
|
||||
|
||||
- 'migration_number': The integer representing the current state of
|
||||
the migrations
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
# Imports that other modules might use
|
||||
from pymongo import ASCENDING, DESCENDING
|
||||
from pymongo.errors import InvalidId
|
||||
from mongokit import ObjectId
|
||||
|
||||
from mediagoblin.db.mongo.indexes import ACTIVE_INDEXES, DEPRECATED_INDEXES
|
||||
|
||||
|
||||
################
|
||||
# Indexing tools
|
||||
################
|
||||
|
||||
|
||||
def add_new_indexes(database, active_indexes=ACTIVE_INDEXES):
|
||||
"""
|
||||
Add any new indexes to the database.
|
||||
|
||||
Args:
|
||||
- database: pymongo or mongokit database instance.
|
||||
- active_indexes: indexes to possibly add in the pattern of:
|
||||
{'collection_name': {
|
||||
'identifier': {
|
||||
'index': [index_foo_goes_here],
|
||||
'unique': True}}
|
||||
where 'index' is the index to add and all other options are
|
||||
arguments for collection.create_index.
|
||||
|
||||
Returns:
|
||||
A list of indexes added in form ('collection', 'index_name')
|
||||
"""
|
||||
indexes_added = []
|
||||
|
||||
for collection_name, indexes in active_indexes.iteritems():
|
||||
collection = database[collection_name]
|
||||
collection_indexes = collection.index_information().keys()
|
||||
|
||||
for index_name, index_data in indexes.iteritems():
|
||||
if not index_name in collection_indexes:
|
||||
# Get a copy actually so we don't modify the actual
|
||||
# structure
|
||||
index_data = copy.copy(index_data)
|
||||
index = index_data.pop('index')
|
||||
collection.create_index(
|
||||
index, name=index_name, **index_data)
|
||||
|
||||
indexes_added.append((collection_name, index_name))
|
||||
|
||||
return indexes_added
|
||||
|
||||
|
||||
def remove_deprecated_indexes(database, deprecated_indexes=DEPRECATED_INDEXES):
|
||||
"""
|
||||
Remove any deprecated indexes from the database.
|
||||
|
||||
Args:
|
||||
- database: pymongo or mongokit database instance.
|
||||
- deprecated_indexes: the indexes to deprecate in the pattern of:
|
||||
{'collection_name': {
|
||||
'identifier': {
|
||||
'index': [index_foo_goes_here],
|
||||
'unique': True}}
|
||||
|
||||
(... although we really only need the 'identifier' here, as the
|
||||
rest of the information isn't used in this case. But it's kept
|
||||
around so we can remember what it was)
|
||||
|
||||
Returns:
|
||||
A list of indexes removed in form ('collection', 'index_name')
|
||||
"""
|
||||
indexes_removed = []
|
||||
|
||||
for collection_name, indexes in deprecated_indexes.iteritems():
|
||||
collection = database[collection_name]
|
||||
collection_indexes = collection.index_information().keys()
|
||||
|
||||
for index_name, index_data in indexes.iteritems():
|
||||
if index_name in collection_indexes:
|
||||
collection.drop_index(index_name)
|
||||
|
||||
indexes_removed.append((collection_name, index_name))
|
||||
|
||||
return indexes_removed
|
||||
|
||||
|
||||
#################
|
||||
# Migration tools
|
||||
#################
|
||||
|
||||
# The default migration registry...
|
||||
#
|
||||
# Don't set this yourself! RegisterMigration will automatically fill
|
||||
# this with stuff via decorating methods in migrations.py
|
||||
|
||||
class MissingCurrentMigration(Exception):
|
||||
pass
|
||||
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
class RegisterMigration(object):
|
||||
"""
|
||||
Tool for registering migrations
|
||||
|
||||
Call like:
|
||||
|
||||
@RegisterMigration(33)
|
||||
def update_dwarves(database):
|
||||
[...]
|
||||
|
||||
This will register your migration with the default migration
|
||||
registry. Alternately, to specify a very specific
|
||||
migration_registry, you can pass in that as the second argument.
|
||||
|
||||
Note, the number of your migration should NEVER be 0 or less than
|
||||
0. 0 is the default "no migrations" state!
|
||||
"""
|
||||
def __init__(self, migration_number, migration_registry=MIGRATIONS):
|
||||
assert migration_number > 0, "Migration number must be > 0!"
|
||||
assert migration_number not in migration_registry, \
|
||||
"Duplicate migration numbers detected! That's not allowed!"
|
||||
|
||||
self.migration_number = migration_number
|
||||
self.migration_registry = migration_registry
|
||||
|
||||
def __call__(self, migration):
|
||||
self.migration_registry[self.migration_number] = migration
|
||||
return migration
|
||||
|
||||
|
||||
class MigrationManager(object):
|
||||
"""
|
||||
Migration handling tool.
|
||||
|
||||
Takes information about a database, lets you update the database
|
||||
to the latest migrations, etc.
|
||||
"""
|
||||
def __init__(self, database, migration_registry=MIGRATIONS):
|
||||
"""
|
||||
Args:
|
||||
- database: database we're going to migrate
|
||||
- migration_registry: where we should find all migrations to
|
||||
run
|
||||
"""
|
||||
self.database = database
|
||||
self.migration_registry = migration_registry
|
||||
self._sorted_migrations = None
|
||||
|
||||
def _ensure_current_migration_record(self):
|
||||
"""
|
||||
If there isn't a database[u'app_metadata'] mediagoblin entry
|
||||
with the 'current_migration', throw an error.
|
||||
"""
|
||||
if self.database_current_migration() is None:
|
||||
raise MissingCurrentMigration(
|
||||
"Tried to call function which requires "
|
||||
"'current_migration' set in database")
|
||||
|
||||
@property
|
||||
def sorted_migrations(self):
|
||||
"""
|
||||
Sort migrations if necessary and store in self._sorted_migrations
|
||||
"""
|
||||
if not self._sorted_migrations:
|
||||
self._sorted_migrations = sorted(
|
||||
self.migration_registry.items(),
|
||||
# sort on the key... the migration number
|
||||
key=lambda migration_tuple: migration_tuple[0])
|
||||
|
||||
return self._sorted_migrations
|
||||
|
||||
def latest_migration(self):
|
||||
"""
|
||||
Return a migration number for the latest migration, or 0 if
|
||||
there are no migrations.
|
||||
"""
|
||||
if self.sorted_migrations:
|
||||
return self.sorted_migrations[-1][0]
|
||||
else:
|
||||
# If no migrations have been set, we start at 0.
|
||||
return 0
|
||||
|
||||
def set_current_migration(self, migration_number):
|
||||
"""
|
||||
Set the migration in the database to migration_number
|
||||
"""
|
||||
# Add the mediagoblin migration if necessary
|
||||
self.database[u'app_metadata'].update(
|
||||
{u'_id': u'mediagoblin'},
|
||||
{u'$set': {u'current_migration': migration_number}},
|
||||
upsert=True)
|
||||
|
||||
def install_migration_version_if_missing(self):
|
||||
"""
|
||||
Sets the migration to the latest version if no migration
|
||||
version at all is set.
|
||||
"""
|
||||
mgoblin_metadata = self.database[u'app_metadata'].find_one(
|
||||
{u'_id': u'mediagoblin'})
|
||||
if not mgoblin_metadata:
|
||||
latest_migration = self.latest_migration()
|
||||
self.set_current_migration(latest_migration)
|
||||
|
||||
def database_current_migration(self):
|
||||
"""
|
||||
Return the current migration in the database.
|
||||
"""
|
||||
mgoblin_metadata = self.database[u'app_metadata'].find_one(
|
||||
{u'_id': u'mediagoblin'})
|
||||
if not mgoblin_metadata:
|
||||
return None
|
||||
else:
|
||||
return mgoblin_metadata[u'current_migration']
|
||||
|
||||
def database_at_latest_migration(self):
|
||||
"""
|
||||
See if the database is at the latest migration.
|
||||
Returns a boolean.
|
||||
"""
|
||||
current_migration = self.database_current_migration()
|
||||
return current_migration == self.latest_migration()
|
||||
|
||||
def migrations_to_run(self):
|
||||
"""
|
||||
Get a list of migrations to run still, if any.
|
||||
|
||||
Note that calling this will set your migration version to the
|
||||
latest version if it isn't installed to anything yet!
|
||||
"""
|
||||
self._ensure_current_migration_record()
|
||||
|
||||
db_current_migration = self.database_current_migration()
|
||||
|
||||
return [
|
||||
(migration_number, migration_func)
|
||||
for migration_number, migration_func in self.sorted_migrations
|
||||
if migration_number > db_current_migration]
|
||||
|
||||
def migrate_new(self, pre_callback=None, post_callback=None):
|
||||
"""
|
||||
Run all migrations.
|
||||
|
||||
Includes two optional args:
|
||||
- pre_callback: if called, this is a callback on something to
|
||||
run pre-migration. Takes (migration_number, migration_func)
|
||||
as arguments
|
||||
- pre_callback: if called, this is a callback on something to
|
||||
run post-migration. Takes (migration_number, migration_func)
|
||||
as arguments
|
||||
"""
|
||||
# If we aren't set to any version number, presume we're at the
|
||||
# latest (which means we'll do nothing here...)
|
||||
self.install_migration_version_if_missing()
|
||||
|
||||
for migration_number, migration_func in self.migrations_to_run():
|
||||
if pre_callback:
|
||||
pre_callback(migration_number, migration_func)
|
||||
migration_func(self.database)
|
||||
self.set_current_migration(migration_number)
|
||||
if post_callback:
|
||||
post_callback(migration_number, migration_func)
|
||||
|
||||
|
||||
##########################
|
||||
# Random utility functions
|
||||
##########################
|
||||
|
||||
|
||||
def atomic_update(table, query_dict, update_values):
|
||||
table.collection.update(
|
||||
query_dict,
|
||||
{"$set": update_values})
|
||||
|
||||
|
||||
def check_media_slug_used(db, uploader_id, slug, ignore_m_id):
|
||||
query_dict = {'uploader': uploader_id, 'slug': slug}
|
||||
if ignore_m_id is not None:
|
||||
query_dict['_id'] = {'$ne': ignore_m_id}
|
||||
existing_user_slug_entries = db.MediaEntry.find(
|
||||
query_dict).count()
|
||||
return existing_user_slug_entries
|
||||
|
||||
|
||||
def media_entries_for_tag_slug(db, tag_slug):
|
||||
return db.MediaEntry.find(
|
||||
{u'state': u'processed',
|
||||
u'tags.slug': tag_slug})
|
@ -14,16 +14,88 @@
|
||||
# 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/>.
|
||||
|
||||
try:
|
||||
from mediagoblin.db.sql_switch import use_sql
|
||||
except ImportError:
|
||||
use_sql = False
|
||||
|
||||
if use_sql:
|
||||
from mediagoblin.db.sql.open import \
|
||||
setup_connection_and_db_from_config, check_db_migrations_current, \
|
||||
load_models
|
||||
else:
|
||||
from mediagoblin.db.mongo.open import \
|
||||
setup_connection_and_db_from_config, check_db_migrations_current, \
|
||||
load_models
|
||||
from sqlalchemy import create_engine, event
|
||||
import logging
|
||||
|
||||
from mediagoblin.db.base import Base, Session
|
||||
from mediagoblin import mg_globals
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseMaster(object):
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
|
||||
for k, v in Base._decl_class_registry.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def commit(self):
|
||||
Session.commit()
|
||||
|
||||
def save(self, obj):
|
||||
Session.add(obj)
|
||||
Session.flush()
|
||||
|
||||
def check_session_clean(self):
|
||||
for dummy in Session():
|
||||
_log.warn("STRANGE: There are elements in the sql session. "
|
||||
"Please report this and help us track this down.")
|
||||
break
|
||||
|
||||
def reset_after_request(self):
|
||||
Session.rollback()
|
||||
Session.remove()
|
||||
|
||||
|
||||
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:
|
||||
__import__(plugin + ".models")
|
||||
except ImportError as exc:
|
||||
_log.debug("Could not load {0}.models: {1}".format(
|
||||
plugin,
|
||||
exc))
|
||||
|
||||
|
||||
def _sqlite_fk_pragma_on_connect(dbapi_con, con_record):
|
||||
"""Enable foreign key checking on each new sqlite connection"""
|
||||
dbapi_con.execute('pragma foreign_keys=on')
|
||||
|
||||
|
||||
def _sqlite_disable_fk_pragma_on_connect(dbapi_con, con_record):
|
||||
"""
|
||||
Disable foreign key checking on each new sqlite connection
|
||||
(Good for migrations!)
|
||||
"""
|
||||
dbapi_con.execute('pragma foreign_keys=off')
|
||||
|
||||
|
||||
def setup_connection_and_db_from_config(app_config, migrations=False):
|
||||
engine = create_engine(app_config['sql_engine'])
|
||||
|
||||
# Enable foreign key checking for sqlite
|
||||
if app_config['sql_engine'].startswith('sqlite://'):
|
||||
if migrations:
|
||||
event.listen(engine, 'connect',
|
||||
_sqlite_disable_fk_pragma_on_connect)
|
||||
else:
|
||||
event.listen(engine, 'connect', _sqlite_fk_pragma_on_connect)
|
||||
|
||||
# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
||||
|
||||
Session.configure(bind=engine)
|
||||
|
||||
return DatabaseMaster(engine)
|
||||
|
||||
|
||||
def check_db_migrations_current(db):
|
||||
pass
|
||||
|
@ -1,282 +0,0 @@
|
||||
# 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 copy import copy
|
||||
from itertools import chain, imap
|
||||
|
||||
from mediagoblin.init import setup_global_and_app_config
|
||||
|
||||
from mediagoblin.db.sql.base import Session
|
||||
from mediagoblin.db.sql.models_v0 import Base_v0
|
||||
from mediagoblin.db.sql.models_v0 import (User, MediaEntry, MediaComment,
|
||||
Tag, MediaTag, MediaFile, MediaAttachmentFile, MigrationData,
|
||||
ImageData, VideoData, AsciiData, AudioData)
|
||||
from mediagoblin.db.sql.open import setup_connection_and_db_from_config as \
|
||||
sql_connect
|
||||
from mediagoblin.db.mongo.open import setup_connection_and_db_from_config as \
|
||||
mongo_connect
|
||||
|
||||
|
||||
obj_id_table = dict()
|
||||
|
||||
|
||||
def add_obj_ids(entry, new_entry):
|
||||
global obj_id_table
|
||||
print "\t%r -> SQL id %r" % (entry._id, new_entry.id)
|
||||
obj_id_table[entry._id] = new_entry.id
|
||||
|
||||
|
||||
def copy_attrs(entry, new_entry, attr_list):
|
||||
for a in attr_list:
|
||||
val = entry[a]
|
||||
setattr(new_entry, a, val)
|
||||
|
||||
|
||||
def copy_reference_attr(entry, new_entry, ref_attr):
|
||||
val = entry[ref_attr]
|
||||
val = obj_id_table[val]
|
||||
setattr(new_entry, ref_attr, val)
|
||||
|
||||
|
||||
def convert_users(mk_db):
|
||||
session = Session()
|
||||
|
||||
for entry in mk_db.User.find().sort('created'):
|
||||
print entry.username
|
||||
|
||||
new_entry = User()
|
||||
copy_attrs(entry, new_entry,
|
||||
('username', 'email', 'created', 'pw_hash', 'email_verified',
|
||||
'status', 'verification_key', 'is_admin', 'url',
|
||||
'bio',
|
||||
'fp_verification_key', 'fp_token_expire',))
|
||||
# new_entry.fp_verification_expire = entry.fp_token_expire
|
||||
|
||||
session.add(new_entry)
|
||||
session.flush()
|
||||
add_obj_ids(entry, new_entry)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def convert_media_entries(mk_db):
|
||||
session = Session()
|
||||
|
||||
for entry in mk_db.MediaEntry.find().sort('created'):
|
||||
print repr(entry.title)
|
||||
|
||||
new_entry = MediaEntry()
|
||||
copy_attrs(entry, new_entry,
|
||||
('title', 'slug', 'created',
|
||||
'description',
|
||||
'media_type', 'state', 'license',
|
||||
'fail_error', 'fail_metadata',
|
||||
'queued_task_id',))
|
||||
copy_reference_attr(entry, new_entry, "uploader")
|
||||
|
||||
session.add(new_entry)
|
||||
session.flush()
|
||||
add_obj_ids(entry, new_entry)
|
||||
|
||||
for key, value in entry.media_files.iteritems():
|
||||
new_file = MediaFile(name=key, file_path=value)
|
||||
new_file.media_entry = new_entry.id
|
||||
Session.add(new_file)
|
||||
|
||||
for attachment in entry.attachment_files:
|
||||
new_attach = MediaAttachmentFile(
|
||||
name=attachment["name"],
|
||||
filepath=attachment["filepath"],
|
||||
created=attachment["created"]
|
||||
)
|
||||
new_attach.media_entry = new_entry.id
|
||||
Session.add(new_attach)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def convert_image(mk_db):
|
||||
session = Session()
|
||||
|
||||
for media in mk_db.MediaEntry.find(
|
||||
{'media_type': 'mediagoblin.media_types.image'}).sort('created'):
|
||||
media_data = copy(media.media_data)
|
||||
|
||||
if len(media_data):
|
||||
media_data_row = ImageData(**media_data)
|
||||
media_data_row.media_entry = obj_id_table[media['_id']]
|
||||
session.add(media_data_row)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def convert_video(mk_db):
|
||||
session = Session()
|
||||
|
||||
for media in mk_db.MediaEntry.find(
|
||||
{'media_type': 'mediagoblin.media_types.video'}).sort('created'):
|
||||
media_data_row = VideoData(**media.media_data)
|
||||
media_data_row.media_entry = obj_id_table[media['_id']]
|
||||
session.add(media_data_row)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def convert_media_tags(mk_db):
|
||||
session = Session()
|
||||
session.autoflush = False
|
||||
|
||||
for media in mk_db.MediaEntry.find().sort('created'):
|
||||
print repr(media.title)
|
||||
|
||||
for otag in media.tags:
|
||||
print " ", repr((otag["slug"], otag["name"]))
|
||||
|
||||
nslug = session.query(Tag).filter_by(slug=otag["slug"]).first()
|
||||
print " ", repr(nslug)
|
||||
if nslug is None:
|
||||
nslug = Tag(slug=otag["slug"])
|
||||
session.add(nslug)
|
||||
session.flush()
|
||||
print " ", repr(nslug), nslug.id
|
||||
|
||||
ntag = MediaTag()
|
||||
ntag.tag = nslug.id
|
||||
ntag.name = otag["name"]
|
||||
ntag.media_entry = obj_id_table[media._id]
|
||||
session.add(ntag)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def convert_media_comments(mk_db):
|
||||
session = Session()
|
||||
|
||||
for entry in mk_db.MediaComment.find().sort('created'):
|
||||
print repr(entry.content)
|
||||
|
||||
new_entry = MediaComment()
|
||||
copy_attrs(entry, new_entry,
|
||||
('created',
|
||||
'content',))
|
||||
|
||||
try:
|
||||
copy_reference_attr(entry, new_entry, "media_entry")
|
||||
copy_reference_attr(entry, new_entry, "author")
|
||||
except KeyError as e:
|
||||
print('KeyError in convert_media_comments(): {0}'.format(e))
|
||||
else:
|
||||
session.add(new_entry)
|
||||
session.flush()
|
||||
add_obj_ids(entry, new_entry)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
media_types_tables = (
|
||||
("mediagoblin.media_types.image", (ImageData,)),
|
||||
("mediagoblin.media_types.video", (VideoData,)),
|
||||
("mediagoblin.media_types.ascii", (AsciiData,)),
|
||||
("mediagoblin.media_types.audio", (AudioData,)),
|
||||
)
|
||||
|
||||
|
||||
def convert_add_migration_versions(dummy_sql_db):
|
||||
session = Session()
|
||||
|
||||
for name in chain(("__main__",),
|
||||
imap(lambda e: e[0], media_types_tables)):
|
||||
print "\tAdding %s" % (name,)
|
||||
m = MigrationData(name=unicode(name), version=0)
|
||||
session.add(m)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def cleanup_sql_tables(sql_db):
|
||||
for mt, table_list in media_types_tables:
|
||||
session = Session()
|
||||
|
||||
count = session.query(MediaEntry.media_type). \
|
||||
filter_by(media_type=unicode(mt)).count()
|
||||
print " %s: %d entries" % (mt, count)
|
||||
|
||||
if count == 0:
|
||||
print "\tAnalyzing tables"
|
||||
for tab in table_list:
|
||||
cnt2 = session.query(tab).count()
|
||||
print "\t %s: %d entries" % (tab.__tablename__, cnt2)
|
||||
assert cnt2 == 0
|
||||
|
||||
print "\tRemoving migration info"
|
||||
mi = session.query(MigrationData).filter_by(name=unicode(mt)).one()
|
||||
session.delete(mi)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
print "\tDropping tables"
|
||||
tables = [model.__table__ for model in table_list]
|
||||
Base_v0.metadata.drop_all(sql_db.engine, tables=tables)
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
def print_header(title):
|
||||
print "\n=== %s ===" % (title,)
|
||||
|
||||
|
||||
convert_call_list = (
|
||||
("Converting Users", convert_users),
|
||||
("Converting Media Entries", convert_media_entries),
|
||||
("Converting Media Data for Images", convert_image),
|
||||
("Cnnverting Media Data for Videos", convert_video),
|
||||
("Converting Tags for Media", convert_media_tags),
|
||||
("Converting Media Comments", convert_media_comments),
|
||||
)
|
||||
|
||||
sql_call_list = (
|
||||
("Filling Migration Tables", convert_add_migration_versions),
|
||||
("Analyzing/Cleaning SQL Data", cleanup_sql_tables),
|
||||
)
|
||||
|
||||
def run_conversion(config_name):
|
||||
global_config, app_config = setup_global_and_app_config(config_name)
|
||||
|
||||
sql_conn, sql_db = sql_connect(app_config)
|
||||
mk_conn, mk_db = mongo_connect(app_config)
|
||||
|
||||
Base_v0.metadata.create_all(sql_db.engine)
|
||||
|
||||
for title, func in convert_call_list:
|
||||
print_header(title)
|
||||
func(mk_db)
|
||||
Session.remove()
|
||||
|
||||
for title, func in sql_call_list:
|
||||
print_header(title)
|
||||
func(sql_db)
|
||||
Session.remove()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_conversion("mediagoblin.ini")
|
@ -1,118 +0,0 @@
|
||||
# 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 datetime
|
||||
|
||||
from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger,
|
||||
Integer, Unicode, UnicodeText, DateTime, ForeignKey)
|
||||
|
||||
from mediagoblin.db.sql.util import RegisterMigration
|
||||
from mediagoblin.db.sql.models import MediaEntry, Collection, User, \
|
||||
ProcessingMetaData
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
@RegisterMigration(1, MIGRATIONS)
|
||||
def ogg_to_webm_audio(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
file_keynames = Table('core__file_keynames', metadata, autoload=True,
|
||||
autoload_with=db_conn.bind)
|
||||
|
||||
db_conn.execute(
|
||||
file_keynames.update().where(file_keynames.c.name == 'ogg').
|
||||
values(name='webm_audio')
|
||||
)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(2, MIGRATIONS)
|
||||
def add_wants_notification_column(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
users = Table('core__users', metadata, autoload=True,
|
||||
autoload_with=db_conn.bind)
|
||||
|
||||
col = Column('wants_comment_notification', Boolean,
|
||||
default=True, nullable=True)
|
||||
col.create(users, populate_defaults=True)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(3, MIGRATIONS)
|
||||
def add_transcoding_progress(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
media_entry = Table('core__media_entries', metadata, autoload=True,
|
||||
autoload_with=db_conn.bind)
|
||||
|
||||
col = Column('transcoding_progress', SmallInteger)
|
||||
col.create(media_entry)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(4, MIGRATIONS)
|
||||
def add_collection_tables(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
collection = Table('core__collections', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('title', Unicode, nullable=False),
|
||||
Column('slug', Unicode),
|
||||
Column('created', DateTime, nullable=False, default=datetime.datetime.now, index=True),
|
||||
Column('description', UnicodeText),
|
||||
Column('creator', Integer, ForeignKey(User.id), nullable=False),
|
||||
Column('items', Integer, default=0))
|
||||
|
||||
collection_item = Table('core__collection_items', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('media_entry', Integer, ForeignKey(MediaEntry.id), nullable=False, index=True),
|
||||
Column('collection', Integer, ForeignKey(Collection.id), nullable=False),
|
||||
Column('note', UnicodeText, nullable=True),
|
||||
Column('added', DateTime, nullable=False, default=datetime.datetime.now),
|
||||
Column('position', Integer))
|
||||
|
||||
collection.create()
|
||||
collection_item.create()
|
||||
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(5, MIGRATIONS)
|
||||
def add_mediaentry_collected(db_conn):
|
||||
metadata = MetaData(bind=db_conn.bind)
|
||||
|
||||
media_entry = Table('core__media_entries', metadata, autoload=True,
|
||||
autoload_with=db_conn.bind)
|
||||
|
||||
col = Column('collected', Integer, default=0)
|
||||
col.create(media_entry)
|
||||
db_conn.commit()
|
||||
|
||||
|
||||
@RegisterMigration(6, MIGRATIONS)
|
||||
def create_processing_metadata_table(db):
|
||||
metadata = MetaData(bind=db.bind)
|
||||
|
||||
metadata_table = Table('core__processing_metadata', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('media_entry_id', Integer, ForeignKey(MediaEntry.id),
|
||||
nullable=False, index=True),
|
||||
Column('callback_url', Unicode))
|
||||
|
||||
metadata_table.create()
|
||||
db.commit()
|
@ -1,78 +0,0 @@
|
||||
# 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 create_engine
|
||||
import logging
|
||||
|
||||
from mediagoblin.db.sql.base import Base, Session
|
||||
from mediagoblin import mg_globals
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseMaster(object):
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
|
||||
for k, v in Base._decl_class_registry.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def commit(self):
|
||||
Session.commit()
|
||||
|
||||
def save(self, obj):
|
||||
Session.add(obj)
|
||||
Session.flush()
|
||||
|
||||
def check_session_clean(self):
|
||||
for dummy in Session():
|
||||
_log.warn("STRANGE: There are elements in the sql session. "
|
||||
"Please report this and help us track this down.")
|
||||
break
|
||||
|
||||
def reset_after_request(self):
|
||||
Session.rollback()
|
||||
Session.remove()
|
||||
|
||||
|
||||
def load_models(app_config):
|
||||
import mediagoblin.db.sql.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:
|
||||
__import__(plugin + ".models")
|
||||
except ImportError as exc:
|
||||
_log.debug("Could not load {0}.models: {1}".format(
|
||||
plugin,
|
||||
exc))
|
||||
|
||||
|
||||
def setup_connection_and_db_from_config(app_config):
|
||||
engine = create_engine(app_config['sql_engine'])
|
||||
# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
||||
Session.configure(bind=engine)
|
||||
|
||||
return "dummy conn", DatabaseMaster(engine)
|
||||
|
||||
|
||||
def check_db_migrations_current(db):
|
||||
pass
|
@ -1 +0,0 @@
|
||||
use_sql = True
|
@ -14,16 +14,63 @@
|
||||
# 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/>.
|
||||
|
||||
try:
|
||||
from mediagoblin.db.sql_switch import use_sql
|
||||
except ImportError:
|
||||
use_sql = False
|
||||
from mediagoblin.db.base import Session
|
||||
from mediagoblin.db.models import MediaEntry, Tag, MediaTag, Collection
|
||||
|
||||
if use_sql:
|
||||
from mediagoblin.db.sql.fake import ObjectId, InvalidId, DESCENDING
|
||||
from mediagoblin.db.sql.util import atomic_update, check_media_slug_used, \
|
||||
media_entries_for_tag_slug, check_collection_slug_used
|
||||
else:
|
||||
from mediagoblin.db.mongo.util import \
|
||||
ObjectId, InvalidId, DESCENDING, atomic_update, \
|
||||
check_media_slug_used, media_entries_for_tag_slug
|
||||
|
||||
##########################
|
||||
# Random utility functions
|
||||
##########################
|
||||
|
||||
|
||||
def atomic_update(table, query_dict, update_values):
|
||||
table.find(query_dict).update(update_values,
|
||||
synchronize_session=False)
|
||||
Session.commit()
|
||||
|
||||
|
||||
def check_media_slug_used(uploader_id, slug, ignore_m_id):
|
||||
query = MediaEntry.query.filter_by(uploader=uploader_id, slug=slug)
|
||||
if ignore_m_id is not None:
|
||||
query = query.filter(MediaEntry.id != ignore_m_id)
|
||||
does_exist = query.first() is not None
|
||||
return does_exist
|
||||
|
||||
|
||||
def media_entries_for_tag_slug(dummy_db, tag_slug):
|
||||
return MediaEntry.query \
|
||||
.join(MediaEntry.tags_helper) \
|
||||
.join(MediaTag.tag_helper) \
|
||||
.filter(
|
||||
(MediaEntry.state == u'processed')
|
||||
& (Tag.slug == tag_slug))
|
||||
|
||||
|
||||
def clean_orphan_tags(commit=True):
|
||||
"""Search for unused MediaTags and delete them"""
|
||||
q1 = Session.query(Tag).outerjoin(MediaTag).filter(MediaTag.id==None)
|
||||
for t in q1:
|
||||
Session.delete(t)
|
||||
# The "let the db do all the work" version:
|
||||
# q1 = Session.query(Tag.id).outerjoin(MediaTag).filter(MediaTag.id==None)
|
||||
# q2 = Session.query(Tag).filter(Tag.id.in_(q1))
|
||||
# q2.delete(synchronize_session = False)
|
||||
if commit:
|
||||
Session.commit()
|
||||
|
||||
|
||||
def check_collection_slug_used(creator_id, slug, ignore_c_id):
|
||||
filt = (Collection.creator == creator_id) \
|
||||
& (Collection.slug == slug)
|
||||
if ignore_c_id is not None:
|
||||
filt = filt & (Collection.id != ignore_c_id)
|
||||
does_exist = Session.query(Collection.id).filter(filt).first() is not None
|
||||
return does_exist
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from mediagoblin.db.open import setup_connection_and_db_from_config
|
||||
|
||||
db = setup_connection_and_db_from_config({'sql_engine':'sqlite:///mediagoblin.db'})
|
||||
|
||||
clean_orphan_tags()
|
||||
|
@ -17,11 +17,11 @@
|
||||
from functools import wraps
|
||||
|
||||
from urlparse import urljoin
|
||||
from urllib import urlencode
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
from werkzeug.urls import url_quote
|
||||
|
||||
from webob import exc
|
||||
|
||||
from mediagoblin.db.util import ObjectId, InvalidId
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.db.models import MediaEntry, User
|
||||
from mediagoblin.tools.response import redirect, render_404
|
||||
|
||||
|
||||
@ -32,26 +32,37 @@ def require_active_login(controller):
|
||||
@wraps(controller)
|
||||
def new_controller_func(request, *args, **kwargs):
|
||||
if request.user and \
|
||||
request.user.get('status') == u'needs_email_verification':
|
||||
request.user.status == u'needs_email_verification':
|
||||
return redirect(
|
||||
request, 'mediagoblin.user_pages.user_home',
|
||||
user=request.user.username)
|
||||
elif not request.user or request.user.get('status') != u'active':
|
||||
elif not request.user or request.user.status != u'active':
|
||||
next_url = urljoin(
|
||||
request.urlgen('mediagoblin.auth.login',
|
||||
qualified=True),
|
||||
request.url)
|
||||
|
||||
return exc.HTTPFound(
|
||||
location='?'.join([
|
||||
request.urlgen('mediagoblin.auth.login'),
|
||||
urlencode({
|
||||
'next': next_url})]))
|
||||
return redirect(request, 'mediagoblin.auth.login',
|
||||
next=next_url)
|
||||
|
||||
return controller(request, *args, **kwargs)
|
||||
|
||||
return new_controller_func
|
||||
|
||||
def active_user_from_url(controller):
|
||||
"""Retrieve User() from <user> URL pattern and pass in as url_user=...
|
||||
|
||||
Returns a 404 if no such active user has been found"""
|
||||
@wraps(controller)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
user = User.query.filter_by(username=request.matchdict['user']).first()
|
||||
if user is None:
|
||||
return render_404(request)
|
||||
|
||||
return controller(request, *args, url_user=user, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def user_may_delete_media(controller):
|
||||
"""
|
||||
@ -59,11 +70,10 @@ def user_may_delete_media(controller):
|
||||
"""
|
||||
@wraps(controller)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
uploader_id = request.db.MediaEntry.find_one(
|
||||
{'_id': ObjectId(request.matchdict['media'])}).uploader
|
||||
uploader_id = kwargs['media'].uploader
|
||||
if not (request.user.is_admin or
|
||||
request.user._id == uploader_id):
|
||||
return exc.HTTPForbidden()
|
||||
request.user.id == uploader_id):
|
||||
raise Forbidden()
|
||||
|
||||
return controller(request, *args, **kwargs)
|
||||
|
||||
@ -79,8 +89,8 @@ def user_may_alter_collection(controller):
|
||||
creator_id = request.db.User.find_one(
|
||||
{'username': request.matchdict['user']}).id
|
||||
if not (request.user.is_admin or
|
||||
request.user._id == creator_id):
|
||||
return exc.HTTPForbidden()
|
||||
request.user.id == creator_id):
|
||||
raise Forbidden()
|
||||
|
||||
return controller(request, *args, **kwargs)
|
||||
|
||||
@ -111,29 +121,34 @@ def get_user_media_entry(controller):
|
||||
"""
|
||||
@wraps(controller)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
user = request.db.User.find_one(
|
||||
{'username': request.matchdict['user']})
|
||||
|
||||
user = User.query.filter_by(username=request.matchdict['user']).first()
|
||||
if not user:
|
||||
return render_404(request)
|
||||
media = request.db.MediaEntry.find_one(
|
||||
{'slug': request.matchdict['media'],
|
||||
'state': u'processed',
|
||||
'uploader': user._id})
|
||||
raise NotFound()
|
||||
|
||||
# no media via slug? Grab it via ObjectId
|
||||
if not media:
|
||||
media = None
|
||||
|
||||
# might not be a slug, might be an id, but whatever
|
||||
media_slug = request.matchdict['media']
|
||||
|
||||
# if it starts with id: it actually isn't a slug, it's an id.
|
||||
if media_slug.startswith(u'id:'):
|
||||
try:
|
||||
media = request.db.MediaEntry.find_one(
|
||||
{'_id': ObjectId(request.matchdict['media']),
|
||||
'state': u'processed',
|
||||
'uploader': user._id})
|
||||
except InvalidId:
|
||||
return render_404(request)
|
||||
media = MediaEntry.query.filter_by(
|
||||
id=int(media_slug[3:]),
|
||||
state=u'processed',
|
||||
uploader=user.id).first()
|
||||
except ValueError:
|
||||
raise NotFound()
|
||||
else:
|
||||
# no magical id: stuff? It's a slug!
|
||||
media = MediaEntry.query.filter_by(
|
||||
slug=media_slug,
|
||||
state=u'processed',
|
||||
uploader=user.id).first()
|
||||
|
||||
# Still no media? Okay, 404.
|
||||
if not media:
|
||||
return render_404(request)
|
||||
if not media:
|
||||
# Didn't find anything? Okay, 404.
|
||||
raise NotFound()
|
||||
|
||||
return controller(request, media=media, *args, **kwargs)
|
||||
|
||||
@ -154,7 +169,7 @@ def get_user_collection(controller):
|
||||
|
||||
collection = request.db.Collection.find_one(
|
||||
{'slug': request.matchdict['collection'],
|
||||
'creator': user._id})
|
||||
'creator': user.id})
|
||||
|
||||
# Still no collection? Okay, 404.
|
||||
if not collection:
|
||||
@ -177,12 +192,8 @@ def get_user_collection_item(controller):
|
||||
if not user:
|
||||
return render_404(request)
|
||||
|
||||
collection = request.db.Collection.find_one(
|
||||
{'slug': request.matchdict['collection'],
|
||||
'creator': user._id})
|
||||
|
||||
collection_item = request.db.CollectionItem.find_one(
|
||||
{'_id': request.matchdict['collection_item'] })
|
||||
{'id': request.matchdict['collection_item'] })
|
||||
|
||||
# Still no collection item? Okay, 404.
|
||||
if not collection_item:
|
||||
@ -199,17 +210,28 @@ def get_media_entry_by_id(controller):
|
||||
"""
|
||||
@wraps(controller)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
try:
|
||||
media = request.db.MediaEntry.find_one(
|
||||
{'_id': ObjectId(request.matchdict['media']),
|
||||
'state': u'processed'})
|
||||
except InvalidId:
|
||||
return render_404(request)
|
||||
|
||||
media = MediaEntry.query.filter_by(
|
||||
id=request.matchdict['media_id'],
|
||||
state=u'processed').first()
|
||||
# Still no media? Okay, 404.
|
||||
if not media:
|
||||
return render_404(request)
|
||||
|
||||
given_username = request.matchdict.get('user')
|
||||
if given_username and (given_username != media.get_uploader.username):
|
||||
return render_404(request)
|
||||
|
||||
return controller(request, media=media, *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
|
||||
|
@ -17,7 +17,7 @@
|
||||
import wtforms
|
||||
|
||||
from mediagoblin.tools.text import tag_length_validator, TOO_LONG_TAG_WARNING
|
||||
from mediagoblin.tools.translate import fake_ugettext_passthrough as _
|
||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||
from mediagoblin.tools.licenses import licenses_as_choices
|
||||
|
||||
class EditForm(wtforms.Form):
|
||||
@ -65,11 +65,21 @@ class EditAccountForm(wtforms.Form):
|
||||
"Enter your old password to prove you own this account."))
|
||||
new_password = wtforms.PasswordField(
|
||||
_('New password'),
|
||||
[wtforms.validators.Length(min=6, max=30)],
|
||||
[
|
||||
wtforms.validators.Optional(),
|
||||
wtforms.validators.Length(min=6, max=30)
|
||||
],
|
||||
id="password")
|
||||
license_preference = wtforms.SelectField(
|
||||
_('License preference'),
|
||||
[
|
||||
wtforms.validators.Optional(),
|
||||
wtforms.validators.AnyOf([lic[0] for lic in licenses_as_choices()]),
|
||||
],
|
||||
choices=licenses_as_choices(),
|
||||
description=_('This will be your default license on upload forms.'))
|
||||
wants_comment_notification = wtforms.BooleanField(
|
||||
_(''),
|
||||
description=_("Email me when others comment on my media"))
|
||||
label=_("Email me when others comment on my media"))
|
||||
|
||||
|
||||
class EditAttachmentsForm(wtforms.Form):
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
def may_edit_media(request, media):
|
||||
"""Check, if the request's user may edit the media details"""
|
||||
if media.uploader == request.user._id:
|
||||
if media.uploader == request.user.id:
|
||||
return True
|
||||
if request.user.is_admin:
|
||||
return True
|
||||
|
@ -14,9 +14,13 @@
|
||||
# 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.routing import add_route
|
||||
from mediagoblin.tools.routing import add_route
|
||||
|
||||
add_route('mediagoblin.edit.profile', '/edit/profile/',
|
||||
add_route('mediagoblin.edit.profile', '/u/<string:user>/edit/',
|
||||
'mediagoblin.edit.views:edit_profile')
|
||||
add_route('mediagoblin.edit.legacy_edit_profile', '/edit/profile/',
|
||||
'mediagoblin.edit.views:legacy_edit_profile')
|
||||
add_route('mediagoblin.edit.account', '/edit/account/',
|
||||
'mediagoblin.edit.views:edit_account')
|
||||
add_route('mediagoblin.edit.delete_account', '/edit/account/delete/',
|
||||
'mediagoblin.edit.views:delete_account')
|
||||
|
@ -14,10 +14,9 @@
|
||||
# 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 webob import exc
|
||||
from cgi import FieldStorage
|
||||
from datetime import datetime
|
||||
|
||||
from werkzeug.exceptions import Forbidden
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from mediagoblin import messages
|
||||
@ -26,22 +25,25 @@ from mediagoblin import mg_globals
|
||||
from mediagoblin.auth import lib as auth_lib
|
||||
from mediagoblin.edit import forms
|
||||
from mediagoblin.edit.lib import may_edit_media
|
||||
from mediagoblin.decorators import require_active_login, get_user_media_entry, \
|
||||
user_may_alter_collection, get_user_collection
|
||||
from mediagoblin.tools.response import render_to_response, redirect
|
||||
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
|
||||
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||
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
|
||||
|
||||
import mimetypes
|
||||
|
||||
|
||||
@get_user_media_entry
|
||||
@get_media_entry_by_id
|
||||
@require_active_login
|
||||
def edit_media(request, media):
|
||||
if not may_edit_media(request, media):
|
||||
return exc.HTTPForbidden()
|
||||
raise Forbidden("User may not edit this media")
|
||||
|
||||
defaults = dict(
|
||||
title=media.title,
|
||||
@ -57,29 +59,26 @@ def edit_media(request, media):
|
||||
if request.method == 'POST' and form.validate():
|
||||
# Make sure there isn't already a MediaEntry with such a slug
|
||||
# and userid.
|
||||
slug_used = check_media_slug_used(request.db, media.uploader,
|
||||
request.form['slug'], media.id)
|
||||
slug = slugify(form.slug.data)
|
||||
slug_used = check_media_slug_used(media.uploader, slug, media.id)
|
||||
|
||||
if slug_used:
|
||||
form.slug.errors.append(
|
||||
_(u'An entry with that slug already exists for this user.'))
|
||||
else:
|
||||
media.title = unicode(request.form['title'])
|
||||
media.description = unicode(request.form.get('description'))
|
||||
media.title = form.title.data
|
||||
media.description = form.description.data
|
||||
media.tags = convert_to_tag_list_of_dicts(
|
||||
request.form.get('tags'))
|
||||
|
||||
media.license = unicode(request.form.get('license', '')) or None
|
||||
|
||||
media.slug = unicode(request.form['slug'])
|
||||
form.tags.data)
|
||||
|
||||
media.license = unicode(form.license.data) or None
|
||||
media.slug = slug
|
||||
media.save()
|
||||
|
||||
return exc.HTTPFound(
|
||||
location=media.url_for_self(request.urlgen))
|
||||
return redirect_obj(request, media)
|
||||
|
||||
if request.user.is_admin \
|
||||
and media.uploader != request.user._id \
|
||||
and media.uploader != request.user.id \
|
||||
and request.method != 'POST':
|
||||
messages.add_message(
|
||||
request, messages.WARNING,
|
||||
@ -99,7 +98,7 @@ UNSAFE_MIMETYPES = [
|
||||
'text/svg+xml']
|
||||
|
||||
|
||||
@get_user_media_entry
|
||||
@get_media_entry_by_id
|
||||
@require_active_login
|
||||
def edit_attachments(request, media):
|
||||
if mg_globals.app_config['allow_attachments']:
|
||||
@ -130,7 +129,7 @@ def edit_attachments(request, media):
|
||||
|
||||
attachment_public_filepath \
|
||||
= mg_globals.public_store.get_unique_filepath(
|
||||
['media_entries', unicode(media._id), 'attachment',
|
||||
['media_entries', unicode(media.id), 'attachment',
|
||||
public_filename])
|
||||
|
||||
attachment_public_file = mg_globals.public_store.get_file(
|
||||
@ -143,7 +142,7 @@ def edit_attachments(request, media):
|
||||
request.files['attachment_file'].stream.close()
|
||||
|
||||
media.attachment_files.append(dict(
|
||||
name=request.form['attachment_name'] \
|
||||
name=form.attachment_name.data \
|
||||
or request.files['attachment_file'].filename,
|
||||
filepath=attachment_public_filepath,
|
||||
created=datetime.utcnow(),
|
||||
@ -153,42 +152,50 @@ def edit_attachments(request, media):
|
||||
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS,
|
||||
"You added the attachment %s!" \
|
||||
% (request.form['attachment_name']
|
||||
_("You added the attachment %s!") \
|
||||
% (form.attachment_name.data
|
||||
or request.files['attachment_file'].filename))
|
||||
|
||||
return exc.HTTPFound(
|
||||
location=media.url_for_self(request.urlgen))
|
||||
return redirect(request,
|
||||
location=media.url_for_self(request.urlgen))
|
||||
return render_to_response(
|
||||
request,
|
||||
'mediagoblin/edit/attachments.html',
|
||||
{'media': media,
|
||||
'form': form})
|
||||
else:
|
||||
return exc.HTTPForbidden()
|
||||
raise Forbidden("Attachments are disabled")
|
||||
|
||||
@require_active_login
|
||||
def legacy_edit_profile(request):
|
||||
"""redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
|
||||
username = request.GET.get('username') or request.user.username
|
||||
return redirect(request, 'mediagoblin.edit.profile', user=username)
|
||||
|
||||
|
||||
@require_active_login
|
||||
def edit_profile(request):
|
||||
# admins may edit any user profile given a username in the querystring
|
||||
edit_username = request.GET.get('username')
|
||||
if request.user.is_admin and request.user.username != edit_username:
|
||||
user = request.db.User.find_one({'username': edit_username})
|
||||
@active_user_from_url
|
||||
def edit_profile(request, url_user=None):
|
||||
# admins may edit any user profile
|
||||
if request.user.username != url_user.username:
|
||||
if not request.user.is_admin:
|
||||
raise Forbidden(_("You can only edit your own profile."))
|
||||
|
||||
# No need to warn again if admin just submitted an edited profile
|
||||
if request.method != 'POST':
|
||||
messages.add_message(
|
||||
request, messages.WARNING,
|
||||
_("You are editing a user's profile. Proceed with caution."))
|
||||
else:
|
||||
user = request.user
|
||||
|
||||
user = url_user
|
||||
|
||||
form = forms.EditProfileForm(request.form,
|
||||
url=user.get('url'),
|
||||
bio=user.get('bio'))
|
||||
url=user.url,
|
||||
bio=user.bio)
|
||||
|
||||
if request.method == 'POST' and form.validate():
|
||||
user.url = unicode(request.form['url'])
|
||||
user.bio = unicode(request.form['bio'])
|
||||
user.url = unicode(form.url.data)
|
||||
user.bio = unicode(form.bio.data)
|
||||
|
||||
user.save()
|
||||
|
||||
@ -210,45 +217,42 @@ def edit_profile(request):
|
||||
def edit_account(request):
|
||||
user = request.user
|
||||
form = forms.EditAccountForm(request.form,
|
||||
wants_comment_notification=user.get('wants_comment_notification'))
|
||||
wants_comment_notification=user.wants_comment_notification,
|
||||
license_preference=user.license_preference)
|
||||
|
||||
if request.method == 'POST':
|
||||
form_validated = form.validate()
|
||||
|
||||
#if the user has not filled in the new or old password fields
|
||||
if not form.new_password.data and not form.old_password.data:
|
||||
if form.wants_comment_notification.validate(form):
|
||||
user.wants_comment_notification = \
|
||||
form.wants_comment_notification.data
|
||||
user.save()
|
||||
messages.add_message(request,
|
||||
messages.SUCCESS,
|
||||
_("Account settings saved"))
|
||||
return redirect(request,
|
||||
'mediagoblin.user_pages.user_home',
|
||||
user=user.username)
|
||||
if form_validated and \
|
||||
form.wants_comment_notification.validate(form):
|
||||
user.wants_comment_notification = \
|
||||
form.wants_comment_notification.data
|
||||
|
||||
#so the user has filled in one or both of the password fields
|
||||
else:
|
||||
if form_validated:
|
||||
password_matches = auth_lib.bcrypt_check_password(
|
||||
form.old_password.data,
|
||||
user.pw_hash)
|
||||
if password_matches:
|
||||
#the entire form validates and the password matches
|
||||
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
|
||||
form.new_password.data)
|
||||
user.wants_comment_notification = \
|
||||
form.wants_comment_notification.data
|
||||
user.save()
|
||||
messages.add_message(request,
|
||||
messages.SUCCESS,
|
||||
_("Account settings saved"))
|
||||
return redirect(request,
|
||||
'mediagoblin.user_pages.user_home',
|
||||
user=user.username)
|
||||
else:
|
||||
form.old_password.errors.append(_('Wrong password'))
|
||||
if form_validated and \
|
||||
form.new_password.data or form.old_password.data:
|
||||
password_matches = auth_lib.bcrypt_check_password(
|
||||
form.old_password.data,
|
||||
user.pw_hash)
|
||||
if password_matches:
|
||||
#the entire form validates and the password matches
|
||||
user.pw_hash = auth_lib.bcrypt_gen_password_hash(
|
||||
form.new_password.data)
|
||||
else:
|
||||
form.old_password.errors.append(_('Wrong password'))
|
||||
|
||||
if form_validated and \
|
||||
form.license_preference.validate(form):
|
||||
user.license_preference = \
|
||||
form.license_preference.data
|
||||
|
||||
if form_validated and not form.errors:
|
||||
user.save()
|
||||
messages.add_message(request,
|
||||
messages.SUCCESS,
|
||||
_("Account settings saved"))
|
||||
return redirect(request,
|
||||
'mediagoblin.user_pages.user_home',
|
||||
user=user.username)
|
||||
|
||||
return render_to_response(
|
||||
request,
|
||||
@ -257,6 +261,37 @@ def edit_account(request):
|
||||
'form': form})
|
||||
|
||||
|
||||
@require_active_login
|
||||
def delete_account(request):
|
||||
"""Delete a user completely"""
|
||||
user = request.user
|
||||
if request.method == 'POST':
|
||||
if request.form.get(u'confirmed'):
|
||||
# Form submitted and confirmed. Actually delete the user account
|
||||
# Log out user and delete cookies etc.
|
||||
# TODO: Should we be using MG.auth.views.py:logout for this?
|
||||
request.session.delete()
|
||||
|
||||
# Delete user account and all related media files etc....
|
||||
request.user.delete()
|
||||
|
||||
# We should send a message that the user has been deleted
|
||||
# successfully. But we just deleted the session, so we
|
||||
# can't...
|
||||
return redirect(request, 'index')
|
||||
|
||||
else: # Did not check the confirmation box...
|
||||
messages.add_message(
|
||||
request, messages.WARNING,
|
||||
_('You need to confirm the deletion of your account.'))
|
||||
|
||||
# No POST submission or not confirmed, just show page
|
||||
return render_to_response(
|
||||
request,
|
||||
'mediagoblin/edit/delete_account.html',
|
||||
{'user': user})
|
||||
|
||||
|
||||
@require_active_login
|
||||
@user_may_alter_collection
|
||||
@get_user_collection
|
||||
@ -273,35 +308,33 @@ def edit_collection(request, collection):
|
||||
if request.method == 'POST' and form.validate():
|
||||
# Make sure there isn't already a Collection with such a slug
|
||||
# and userid.
|
||||
slug_used = check_collection_slug_used(request.db, collection.creator,
|
||||
request.form['slug'], collection.id)
|
||||
slug_used = check_collection_slug_used(collection.creator,
|
||||
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':request.form['title']})
|
||||
'creator': request.user.id,
|
||||
'title':form.title.data})
|
||||
|
||||
if existing_collection and existing_collection.id != collection.id:
|
||||
messages.add_message(
|
||||
request, messages.ERROR,
|
||||
_('You already have a collection called "%s"!') % \
|
||||
request.form['title'])
|
||||
form.title.data)
|
||||
elif slug_used:
|
||||
form.slug.errors.append(
|
||||
_(u'A collection with that slug already exists for this user.'))
|
||||
else:
|
||||
collection.title = unicode(request.form['title'])
|
||||
collection.description = unicode(request.form.get('description'))
|
||||
collection.slug = unicode(request.form['slug'])
|
||||
collection.title = unicode(form.title.data)
|
||||
collection.description = unicode(form.description.data)
|
||||
collection.slug = unicode(form.slug.data)
|
||||
|
||||
collection.save()
|
||||
|
||||
return redirect(request, "mediagoblin.user_pages.user_collection",
|
||||
user=collection.get_creator.username,
|
||||
collection=collection.slug)
|
||||
return redirect_obj(request, collection)
|
||||
|
||||
if request.user.is_admin \
|
||||
and collection.creator != request.user._id \
|
||||
and collection.creator != request.user.id \
|
||||
and request.method != 'POST':
|
||||
messages.add_message(
|
||||
request, messages.WARNING,
|
||||
|
@ -25,11 +25,6 @@ SUBCOMMAND_MAP = {
|
||||
'setup': 'mediagoblin.gmg_commands.shell:shell_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.shell:shell',
|
||||
'help': 'Run a shell with some tools pre-setup'},
|
||||
'migrate': {
|
||||
'setup': 'mediagoblin.gmg_commands.migrate:migrate_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.migrate:migrate',
|
||||
'help': ('Migrate your Mongo database. '
|
||||
'[DEPRECATED!] use convert_mongo_to_sql and dbupdate.')},
|
||||
'adduser': {
|
||||
'setup': 'mediagoblin.gmg_commands.users:adduser_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.users:adduser',
|
||||
@ -37,19 +32,15 @@ SUBCOMMAND_MAP = {
|
||||
'makeadmin': {
|
||||
'setup': 'mediagoblin.gmg_commands.users:makeadmin_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.users:makeadmin',
|
||||
'help': 'Changes a user\'s password'},
|
||||
'help': 'Makes user an admin'},
|
||||
'changepw': {
|
||||
'setup': 'mediagoblin.gmg_commands.users:changepw_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.users:changepw',
|
||||
'help': 'Makes admin an user'},
|
||||
'help': 'Changes a user\'s password'},
|
||||
'dbupdate': {
|
||||
'setup': 'mediagoblin.gmg_commands.dbupdate:dbupdate_parse_setup',
|
||||
'func': 'mediagoblin.gmg_commands.dbupdate:dbupdate',
|
||||
'help': 'Set up or update the SQL database'},
|
||||
'convert_mongo_to_sql': {
|
||||
'setup': 'mediagoblin.gmg_commands.mongosql:mongosql_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.mongosql:mongosql',
|
||||
'help': 'Convert Mongo DB data to SQL DB data'},
|
||||
'theme': {
|
||||
'setup': 'mediagoblin.gmg_commands.theme:theme_parser_setup',
|
||||
'func': 'mediagoblin.gmg_commands.theme:theme',
|
||||
|
@ -18,8 +18,8 @@ import logging
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from mediagoblin.db.sql.open import setup_connection_and_db_from_config
|
||||
from mediagoblin.db.sql.util import MigrationManager
|
||||
from mediagoblin.db.open import setup_connection_and_db_from_config
|
||||
from mediagoblin.db.migration_tools import MigrationManager
|
||||
from mediagoblin.init import setup_global_and_app_config
|
||||
from mediagoblin.tools.common import import_component
|
||||
|
||||
@ -52,8 +52,8 @@ def gather_database_data(media_types, plugins):
|
||||
managed_dbdata = []
|
||||
|
||||
# Add main first
|
||||
from mediagoblin.db.sql.models import MODELS as MAIN_MODELS
|
||||
from mediagoblin.db.sql.migrations import MIGRATIONS as MAIN_MIGRATIONS
|
||||
from mediagoblin.db.models import MODELS as MAIN_MODELS
|
||||
from mediagoblin.db.migrations import MIGRATIONS as MAIN_MIGRATIONS
|
||||
|
||||
managed_dbdata.append(
|
||||
DatabaseData(
|
||||
@ -78,6 +78,7 @@ def gather_database_data(media_types, plugins):
|
||||
except AttributeError as exc:
|
||||
_log.warning('Could not find MODELS in {0}.models, have you \
|
||||
forgotten to add it? ({1})'.format(plugin, exc))
|
||||
models = []
|
||||
|
||||
try:
|
||||
migrations = import_component('{0}.migrations:MIGRATIONS'.format(
|
||||
@ -91,6 +92,7 @@ forgotten to add it? ({1})'.format(plugin, exc))
|
||||
except AttributeError as exc:
|
||||
_log.debug('Cloud not find MIGRATIONS in {0}.migrations, have you \
|
||||
forgotten to add it? ({1})'.format(plugin, exc))
|
||||
migrations = {}
|
||||
|
||||
if models:
|
||||
managed_dbdata.append(
|
||||
@ -114,7 +116,7 @@ def run_dbupdate(app_config, global_config):
|
||||
global_config.get('plugins', {}).keys())
|
||||
|
||||
# Set up the database
|
||||
connection, db = setup_connection_and_db_from_config(app_config)
|
||||
db = setup_connection_and_db_from_config(app_config, migrations=True)
|
||||
|
||||
Session = sessionmaker(bind=db.engine)
|
||||
|
||||
|
@ -105,7 +105,7 @@ def env_import(args):
|
||||
setup_storage()
|
||||
|
||||
global_config, app_config = setup_global_and_app_config(args.conf_file)
|
||||
connection, db = setup_connection_and_db_from_config(
|
||||
db = setup_connection_and_db_from_config(
|
||||
app_config)
|
||||
|
||||
tf = tarfile.open(
|
||||
@ -243,8 +243,7 @@ def env_export(args):
|
||||
|
||||
setup_storage()
|
||||
|
||||
connection, db = setup_connection_and_db_from_config(
|
||||
app_config)
|
||||
db = setup_connection_and_db_from_config(app_config)
|
||||
|
||||
_export_database(db, args)
|
||||
|
||||
|
@ -1,75 +0,0 @@
|
||||
# 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 sys
|
||||
|
||||
from mediagoblin.init import setup_global_and_app_config
|
||||
|
||||
|
||||
def migrate_parser_setup(subparser):
|
||||
pass
|
||||
|
||||
|
||||
def _print_started_migration(migration_number, migration_func):
|
||||
sys.stdout.write(
|
||||
"Running migration %s, '%s'... " % (
|
||||
migration_number, migration_func.func_name))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _print_finished_migration(migration_number, migration_func):
|
||||
sys.stdout.write("done.\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def migrate(args):
|
||||
run_migrate(args.conf_file)
|
||||
|
||||
|
||||
def run_migrate(conf_file):
|
||||
# This MUST be imported so as to set up the appropriate migrations!
|
||||
from mediagoblin.db.mongo import migrations
|
||||
|
||||
from mediagoblin.db.mongo import util as db_util
|
||||
from mediagoblin.db.mongo.open import setup_connection_and_db_from_config
|
||||
|
||||
global_config, app_config = setup_global_and_app_config(conf_file)
|
||||
|
||||
connection, db = setup_connection_and_db_from_config(
|
||||
app_config, use_pymongo=True)
|
||||
migration_manager = db_util.MigrationManager(db)
|
||||
|
||||
# Clear old indexes
|
||||
print "== Clearing old indexes... =="
|
||||
removed_indexes = db_util.remove_deprecated_indexes(db)
|
||||
|
||||
for collection, index_name in removed_indexes:
|
||||
print "Removed index '%s' in collection '%s'" % (
|
||||
index_name, collection)
|
||||
|
||||
# Migrate
|
||||
print "\n== Applying migrations... =="
|
||||
migration_manager.migrate_new(
|
||||
pre_callback=_print_started_migration,
|
||||
post_callback=_print_finished_migration)
|
||||
|
||||
# Add new indexes
|
||||
print "\n== Adding new indexes... =="
|
||||
new_indexes = db_util.add_new_indexes(db)
|
||||
|
||||
for collection, index_name in new_indexes:
|
||||
print "Added index '%s' to collection '%s'" % (
|
||||
index_name, collection)
|
@ -47,24 +47,21 @@ def py_shell(**user_namespace):
|
||||
|
||||
def ipython_shell(**user_namespace):
|
||||
"""
|
||||
Run a shell for the user using ipython.
|
||||
Run a shell for the user using ipython. Return False if there is no IPython
|
||||
"""
|
||||
try:
|
||||
from IPython import embed
|
||||
except:
|
||||
print "IPython not available... exiting!"
|
||||
return
|
||||
|
||||
return False
|
||||
|
||||
embed(
|
||||
banner1=SHELL_BANNER,
|
||||
user_ns=user_namespace)
|
||||
|
||||
return True
|
||||
|
||||
def shell(args):
|
||||
"""
|
||||
Setup a shell for the user
|
||||
either a normal Python shell
|
||||
or an IPython one
|
||||
Setup a shell for the user either a normal Python shell or an IPython one
|
||||
"""
|
||||
user_namespace = {
|
||||
'mg_globals': mg_globals,
|
||||
@ -74,4 +71,6 @@ def shell(args):
|
||||
if args.ipython:
|
||||
ipython_shell(**user_namespace)
|
||||
else:
|
||||
py_shell(**user_namespace)
|
||||
# Try ipython_shell first and fall back if not available
|
||||
if not ipython_shell(**user_namespace):
|
||||
py_shell(**user_namespace)
|
||||
|
@ -55,7 +55,7 @@ def adduser(args):
|
||||
entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
|
||||
entry.status = u'active'
|
||||
entry.email_verified = True
|
||||
entry.save(validate=True)
|
||||
entry.save()
|
||||
|
||||
print "User created (and email marked as verified)"
|
||||
|
||||
|
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
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
File diff suppressed because it is too large
Load Diff
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
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
BIN
mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.mo
Normal file
BIN
mediagoblin/i18n/he/LC_MESSAGES/mediagoblin.mo
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user