提交 48f96e29 编写于 作者: J Jonathan Thomas

Dramatically simplified querying Files and CLips, added unittest framework...

Dramatically simplified querying Files and CLips, added unittest framework with some initial query unititests, updated all files that called the update manager directly to use the new query interface.
上级 07aa1597
......@@ -33,7 +33,8 @@ from classes.logger import log
from classes.app import get_app
# Get project data reference
project = get_app().project
app = get_app()
project = app.project
class QueryObject:
""" This class allows one or more project data objects to be queried """
......@@ -41,17 +42,50 @@ class QueryObject:
def __init__(self):
""" Constructor """
self.id = None # Unique ID of object
self.key = None # Key path to object in project data
self.data = None # Data dictionary of object
self.id = None # Unique ID of object
self.key = None # Key path to object in project data
self.data = None # Data dictionary of object
self.type = "insert" # Type of operation needed to save
def save(self):
def save(self, OBJECT_TYPE):
""" Save the object back to the project data store """
pass
# Insert or Update this data into the project data store
if not self.id and self.type == "insert":
# Insert record, and Generate id
self.id = project.generate_id()
def delete(self):
# save id in data (if attribute found)
self.data["id"] = self.id
# Set key (if needed)
if not self.key:
self.key = copy.deepcopy(OBJECT_TYPE.object_key)
self.key.append( { "id" : self.id } )
# Insert into project data
app.updates.insert(OBJECT_TYPE.object_key, self.data)
# Mark record as 'update' now... so another call to this method won't insert it again
self.type = "update"
elif self.id and self.type == "update":
# Update existing project data
app.updates.update(self.key, self.data)
def delete(self, OBJECT_TYPE):
""" Delete the object from the project data store """
pass
# Delete if object found and not pending insert
if self.id and self.type == "update":
# Delete from project data store
app.updates.delete(self.key)
self.type = "delete"
def filter(OBJECT_TYPE, **kwargs):
""" Take any arguments given as filters, and find a list of matching objects """
......@@ -76,6 +110,7 @@ class QueryObject:
object.id = child["id"]
object.key = [OBJECT_TYPE.object_name, { "id" : object.id}]
object.data = child
object.type = "update"
matching_objects.append(object)
# Return matching objects
......@@ -97,14 +132,14 @@ class Clip(QueryObject):
""" This class allows one or more project data objects to be queried """
object_name = "clips" # Derived classes should define this
object_key = [object_name] # Derived classes should define this also
def save(self):
""" Save the object back to the project data store """
log.info("Clip save method")
super().save(Clip)
def delete(self):
""" Delete the object from the project data store """
log.info("Clip delete method")
super().delete(Clip)
def filter(**kwargs):
""" Take any arguments given as filters, and find a list of matching objects """
......@@ -119,15 +154,15 @@ class File(QueryObject):
""" This class allows one or more project data objects to be queried """
object_name = "files" # Derived classes should define this
object_key = [object_name] # Derived classes should define this also
def save(self):
""" Save the object back to the project data store """
log.info("File save method")
super().save(File)
def delete(self):
""" Delete the object from the project data store """
log.info("File delete method")
super().delete(File)
def filter(**kwargs):
""" Take any arguments given as filters, and find a list of matching objects """
return QueryObject.filter(File, **kwargs)
......
"""
@file
@brief This file contains unit tests for the Query class
@author Jonathan Thomas <jonathan@openshot.org>
@section LICENSE
Copyright (c) 2008-2014 OpenShot Studios, LLC
(http://www.openshotstudios.com). This file is part of
OpenShot Video Editor (http://www.openshot.org), an open-source project
dedicated to delivering high quality video editing and animation solutions
to the world.
OpenShot Video Editor is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenShot Video Editor 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
"""
import sys, os
# Import parent folder (so it can find other imports)
PATH = os.path.dirname(os.path.dirname( os.path.realpath( __file__) ))
if not PATH in sys.path:
sys.path.append(PATH)
import random
import unittest
import uuid
from classes.app import OpenShotApp
import openshot # Python module for libopenshot (required video editing module installed separately)
try:
import json
except ImportError:
import simplejson as json
class TestQueryClass(unittest.TestCase):
""" Unit test class for Query class """
@classmethod
def setUpClass(TestQueryClass):
""" Init unit test data """
# Create Qt application
TestQueryClass.app = OpenShotApp(sys.argv)
TestQueryClass.clip_ids = []
TestQueryClass.file_ids = []
# Import additional classes that need the app defined first
from classes.query import Clip, File
# Insert some clips into the project data
for num in range(5):
# Create clip
c = openshot.Clip()
# Parse JSON
clip_data = json.loads(c.Json())
# Insert into project data
query_clip = Clip()
query_clip.data = clip_data
query_clip.save()
# Keep track of the ids
TestQueryClass.clip_ids.append(query_clip.id)
# Insert some files into the project data
for num in range(5):
# Create file
r = openshot.DummyReader(openshot.Fraction(24,1), 640, 480, 44100, 2, 30.0)
# Parse JSON
file_data = json.loads(r.Json())
# Insert into project data
query_file = File()
query_file.data = file_data
query_file.data["path"] = os.path.join(PATH, "images", "NoThumbnail.png")
query_file.data["media_type"] = "image"
query_file.save()
# Keep track of the ids
TestQueryClass.file_ids.append(query_file.id)
def test_update_clip(self):
""" Test the Clip.save method """
# Import additional classes that need the app defined first
from classes.query import Clip
# Find a clip named file1
update_id = TestQueryClass.clip_ids[0]
clip = Clip.get(id=update_id)
self.assertTrue(clip)
# Update clip
clip.data["layer"] = "2"
clip.data["title"] = "My Title"
clip.save()
# Verify updated data
# Get clip again
clip = Clip.get(id=update_id)
self.assertEqual(clip.data["layer"], "2")
self.assertEqual(clip.data["title"], "My Title")
def test_delete_clip(self):
""" Test the Clip.delete method """
# Import additional classes that need the app defined first
from classes.query import Clip
# Find a clip named file1
delete_id = TestQueryClass.clip_ids[4]
clip = Clip.get(id=delete_id)
self.assertTrue(clip)
# Delete clip
clip.delete()
# Verify deleted data
clip = Clip.get(id=delete_id)
self.assertFalse(clip)
def test_filter_clip(self):
""" Test the Clip.filter method """
# Import additional classes that need the app defined first
from classes.query import Clip
# Find all clips named file1
clips = Clip.filter(id=TestQueryClass.clip_ids[0])
self.assertTrue(clips)
# Do not find a clip
clips = Clip.filter(id="invalidID")
self.assertEqual(len(clips), 0)
def test_get_clip(self):
""" Test the Clip.get method """
# Import additional classes that need the app defined first
from classes.query import Clip
# Find a clip named file1
clip = Clip.get(id=TestQueryClass.clip_ids[1])
self.assertTrue(clip)
# Do not find a clip
clip = Clip.get(id="invalidID")
self.assertEqual(clip, None)
def test_update_File(self):
""" Test the File.save method """
# Import additional classes that need the app defined first
from classes.query import File
# Find a File named file1
update_id = TestQueryClass.file_ids[0]
file = File.get(id=update_id)
self.assertTrue(file)
# Update File
file.data["height"] = 1080
file.data["width"] = 1920
file.save()
# Verify updated data
# Get File again
file = File.get(id=update_id)
self.assertEqual(file.data["height"], 1080)
self.assertEqual(file.data["width"], 1920)
def test_delete_File(self):
""" Test the File.delete method """
# Import additional classes that need the app defined first
from classes.query import File
# Find a File named file1
delete_id = TestQueryClass.file_ids[4]
file = File.get(id=delete_id)
self.assertTrue(file)
# Delete File
file.delete()
# Verify deleted data
file = File.get(id=delete_id)
self.assertFalse(file)
def test_filter_File(self):
""" Test the File.filter method """
# Import additional classes that need the app defined first
from classes.query import File
# Find all Files named file1
files = File.filter(id=TestQueryClass.file_ids[0])
self.assertTrue(files)
# Do not find a File
files = File.filter(id="invalidID")
self.assertEqual(len(files), 0)
def test_get_File(self):
""" Test the File.get method """
# Import additional classes that need the app defined first
from classes.query import File
# Find a File named file1
file = File.get(id=TestQueryClass.file_ids[1])
self.assertTrue(file)
# Do not find a File
file = File.get(id="invalidID")
self.assertEqual(file, None)
if __name__ == '__main__':
unittest.main()
\ No newline at end of file
......@@ -30,6 +30,7 @@ import os
from urllib.parse import urlparse
from classes import updates
from classes import info
from classes.query import File
from classes.logger import log
from classes.settings import SettingStore
from classes.app import get_app
......@@ -56,8 +57,6 @@ class FilesModel(updates.UpdateInterface):
def update_model(self, clear=True):
log.info("updating files model.")
app = get_app()
proj = app.project
files = proj.get(["files"])
# Get window to check filters
win = app.window
......@@ -70,19 +69,22 @@ class FilesModel(updates.UpdateInterface):
# Add Headers
self.model.setHorizontalHeaderLabels(["Thumb", "Name", "Type" ])
# Get list of files in project
files = File.filter() # get all files
# add item for each file
for file in files:
path, filename = os.path.split(file["path"])
path, filename = os.path.split(file.data["path"])
if not win.actionFilesShowAll.isChecked():
if win.actionFilesShowVideo.isChecked():
if not file["media_type"] == "video":
if not file.data["media_type"] == "video":
continue #to next file, didn't match filter
elif win.actionFilesShowAudio.isChecked():
if not file["media_type"] == "audio":
if not file.data["media_type"] == "audio":
continue #to next file, didn't match filter
elif win.actionFilesShowImage.isChecked():
if not file["media_type"] == "image":
if not file.data["media_type"] == "image":
continue #to next file, didn't match filter
if win.filesFilter.text() != "":
......@@ -90,18 +92,18 @@ class FilesModel(updates.UpdateInterface):
continue
# Generate thumbnail for file (if needed)
if (file["media_type"] == "video" or file["media_type"] == "image"):
if (file.data["media_type"] == "video" or file.data["media_type"] == "image"):
# Determine thumb path
thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file["id"])
thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.id)
# Check if thumb exists
if not os.path.exists(thumb_path):
try:
# Convert path to the correct relative path (based on this folder)
absolute_path_of_file = file["path"]
absolute_path_of_file = file.data["path"]
if not os.path.isabs(absolute_path_of_file):
absolute_path_of_file = os.path.abspath(os.path.join(info.PATH, file["path"]))
absolute_path_of_file = os.path.abspath(os.path.join(info.PATH, file.data["path"]))
relative_path = os.path.relpath(absolute_path_of_file, info.CWD)
# Reload this reader
......@@ -112,7 +114,7 @@ class FilesModel(updates.UpdateInterface):
reader.Open()
# Determine scale of thumbnail
scale = 95.0 / file["width"]
scale = 95.0 / file.data["width"]
# Save thumbnail
reader.GetFrame(0).Save(thumb_path, scale)
......@@ -148,7 +150,7 @@ class FilesModel(updates.UpdateInterface):
# Append Media Type
col = QStandardItem("Type")
col.setData(file["media_type"], Qt.DisplayRole)
col.setData(file.data["media_type"], Qt.DisplayRole)
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
row.append(col)
......@@ -160,14 +162,14 @@ class FilesModel(updates.UpdateInterface):
# Append ID
col = QStandardItem("ID")
col.setData(file["id"], Qt.DisplayRole)
col.setData(file.data["id"], Qt.DisplayRole)
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
row.append(col)
# Append ROW to MODEL (if does not already exist in model)
if not file["id"] in self.model_ids:
if not file.data["id"] in self.model_ids:
self.model.appendRow(row)
self.model_ids[file["id"]] = file["id"]
self.model_ids[file.data["id"]] = file.data["id"]
# Process events in QT (to keep the interface responsive)
app.processEvents()
......
......@@ -32,17 +32,14 @@ from PyQt5.QtCore import QFileInfo, pyqtSlot, QUrl, Qt, QCoreApplication
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import QAbstractSlider, QMenu
from PyQt5.QtWebKitWidgets import QWebView
from classes.query import File, Clip
from classes.logger import log
from classes.app import get_app
from classes import info, updates
from classes.query import File, Clip
import openshot # Python module for libopenshot (required video editing module installed separately)
import uuid
try:
import json
except ImportError:
import simplejson as json
try:
import json
except ImportError:
......@@ -76,26 +73,14 @@ class TimelineWebView(QWebView, updates.UpdateInterface):
clip_data = json.loads(clip_json)
else:
clip_data = clip_json
# Get app
app = get_app()
proj = app.project
clips = proj.get(["clips"])
# Does clip already exist
found_id = None
for existing_clip in clips:
if existing_clip["id"] == clip_data["id"]:
found_id = clip_data["id"]
# find the clip in the project data
if not found_id:
# insert a clip
app.updates.insert(["clips"], clip_data)
else:
# update a clip
app.updates.update(["clips", {"id" : found_id}], clip_data)
# Search for matching clip in project data (if any)
existing_clip = Clip.get(id=clip_data["id"])
if not existing_clip:
# Create a new clip (if not exists)
existing_clip = Clip()
existing_clip.data = clip_data
existing_clip.save()
#Prevent default context menu, and ignore, so that javascript can intercept
def contextMenuEvent(self, event):
......@@ -147,37 +132,31 @@ class TimelineWebView(QWebView, updates.UpdateInterface):
event.accept()
pos = event.posF()
# Find file object in project data
# Get app object
app = get_app()
proj = app.project
files = proj.get(["files"])
# Loop until correct file is found
file = None
for f in files:
if f["id"] == file_id:
file = f
break
# Search for matching file in project data (if any)
file = File.get(id=file_id)
# Bail if no file found
if not file:
event.ignore()
return
if (file["media_type"] == "video" or file["media_type"] == "image"):
if (file.data["media_type"] == "video" or file.data["media_type"] == "image"):
# Determine thumb path
thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file["id"])
thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.data["id"])
else:
# Audio file
thumb_path = os.path.join(info.PATH, "images", "AudioThumbnail.png")
# Get file name
path, filename = os.path.split(file["path"])
path, filename = os.path.split(file.data["path"])
# Convert path to the correct relative path (based on this folder)
absolute_path_of_file = file["path"]
absolute_path_of_file = file.data["path"]
if not os.path.isabs(absolute_path_of_file):
absolute_path_of_file = os.path.abspath(os.path.join(info.PATH, file["path"]))
absolute_path_of_file = os.path.abspath(os.path.join(info.PATH, file.data["path"]))
relative_path = os.path.relpath(absolute_path_of_file, info.CWD)
# Create clip object for this file
......@@ -185,14 +164,13 @@ class TimelineWebView(QWebView, updates.UpdateInterface):
# Append missing attributes to Clip JSON
new_clip = json.loads(c.Json())
new_clip["id"] = str(uuid.uuid1())[:5]
new_clip["file_id"] = file["id"]
new_clip["file_id"] = file.id
new_clip["title"] = filename
new_clip["image"] = thumb_path
# Adjust clip duration, start, and end
new_clip["duration"] = new_clip["reader"]["duration"]
if file["media_type"] != "image":
if file.data["media_type"] != "image":
new_clip["end"] = new_clip["reader"]["duration"]
else:
new_clip["end"] = 8.0 # default to 8 seconds
......
......@@ -28,9 +28,9 @@
import os
from urllib.parse import urlparse
from classes import updates
from classes import info
from classes.logger import log
from classes.query import File
from classes.settings import SettingStore
from classes.app import get_app
from PyQt5.QtCore import QMimeData, QSize, Qt, QCoreApplication, QPoint, QFileInfo
......@@ -119,42 +119,13 @@ class FilesListView(QListView):
# Add file into project
app = get_app()
proj = app.project
files = proj.get(["files"]) #get files list
found = False
new_id = proj.generate_id()
# Check for new_id and filepath in current file list to prevent duplicates
index = 0
sanity_check, sanity_limit = 0, 10
while index < len(files):
if sanity_check > sanity_limit: #Stop checking if we can't get a unique id in 10 tries
break;
f = files[index]
# Stop checking if we find this filepath already in the list, don't allow duplicate.
if f["path"] == filepath:
found = True
break
# Generate new id and start loop over again if duplicate id is found
if f["id"] == new_id: #If this ID is found, generate a new one
new_id = proj.generate_id()
index = 0
sanity_check += 1
continue
# Move to next item
index += 1
if sanity_check > sanity_limit:
msg = QMessageBox()
msg.setText(app._tr("Error generating unique file ID for %s." % filename))
msg.exec_()
return False
if found:
return False
# Check for this path in our existing project data
file = File.get(path=filepath)
# Inspect the file with libopenshot
clip = None
# If this file is already found, exit
if file:
return
# Load filepath in libopenshot clip object (which will try multiple readers to open it)
clip = openshot.Clip(filepath)
......@@ -162,21 +133,20 @@ class FilesListView(QListView):
# Get the JSON for the clip's internal reader
try:
reader = clip.Reader()
file_json = json.loads(reader.Json())
# Add unique ID to the JSON
file_json["id"] = new_id
file_data = json.loads(reader.Json())
# Determine media type
if file_json["has_video"] and not self.is_image(file_json):
file_json["media_type"] = "video"
elif file_json["has_video"] and self.is_image(file_json):
file_json["media_type"] = "image"
elif file_json["has_audio"] and not file_json["has_video"]:
file_json["media_type"] = "audio"
if file_data["has_video"] and not self.is_image(file_data):
file_data["media_type"] = "video"
elif file_data["has_video"] and self.is_image(file_data):
file_data["media_type"] = "image"
elif file_data["has_audio"] and not file_data["has_video"]:
file_data["media_type"] = "audio"
# Add file to files list via update manager
app.updates.insert(["files"], file_json)
# Save new file to the project data
file = File()
file.data = file_data
file.save()
return True
except:
......
......@@ -29,8 +29,8 @@
import os
from urllib.parse import urlparse
from classes import updates
from classes import info
from classes.query import File
from classes.logger import log
from classes.settings import SettingStore
from classes.app import get_app
......@@ -120,42 +120,13 @@ class FilesTreeView(QTreeView):
# Add file into project
app = get_app()
proj = app.project
files = proj.get(["files"]) #get files list
found = False
new_id = proj.generate_id()
# Check for new_id and filepath in current file list to prevent duplicates
index = 0
sanity_check, sanity_limit = 0, 10
while index < len(files):
if sanity_check > sanity_limit: #Stop checking if we can't get a unique id in 10 tries
break;
f = files[index]
# Stop checking if we find this filepath already in the list, don't allow duplicate.
if f["path"] == filepath:
found = True
break
# Generate new id and start loop over again if duplicate id is found
if f["id"] == new_id: #If this ID is found, generate a new one
new_id = proj.generate_id()
index = 0
sanity_check += 1
continue
# Move to next item
index += 1
if sanity_check > sanity_limit:
msg = QMessageBox()
msg.setText(app._tr("Error generating unique file ID for %s." % filename))
msg.exec_()
return False
if found:
return False
# Check for this path in our existing project data
file = File.get(path=filepath)
# Inspect the file with libopenshot
clip = None
# If this file is already found, exit
if file:
return
# Load filepath in libopenshot clip object (which will try multiple readers to open it)
clip = openshot.Clip(filepath)
......@@ -163,21 +134,20 @@ class FilesTreeView(QTreeView):
# Get the JSON for the clip's internal reader
try:
reader = clip.Reader()
file_json = json.loads(reader.Json())
# Add unique ID to the JSON
file_json["id"] = new_id
file_data = json.loads(reader.Json())
# Determine media type
if file_json["has_video"] and not self.is_image(file_json):
file_json["media_type"] = "video"
elif file_json["has_video"] and self.is_image(file_json):
file_json["media_type"] = "image"
elif file_json["has_audio"] and not file_json["has_video"]:
file_json["media_type"] = "audio"
if file_data["has_video"] and not self.is_image(file_data):
file_data["media_type"] = "video"
elif file_data["has_video"] and self.is_image(file_data):
file_data["media_type"] = "image"
elif file_data["has_audio"] and not file_data["has_video"]:
file_data["media_type"] = "audio"
# Add file to files list via update manager
app.updates.insert(["files"], file_json)
# Save new file to the project data
file = File()
file.data = file_data
file.save()
return True
except:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册