diff --git a/docs/Makefile b/docs/Makefile index d0b8f190..62e5f05a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,240 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SPHINXPROJ = Devicehub +SOURCEDIR = . BUILDDIR = _build -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " fasthtml 'fast html': to make HTML files without regenerating the API" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - rm -rf modules +.PHONY: help Makefile -.PHONY: html -html: - sphinx-apidoc -f -l -o modules ../ereuse_devicehub - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: fasthtml -fasthtml: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DeviceHub.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DeviceHub.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/DeviceHub" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DeviceHub" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - sphinx-apidoc -f -l -o . ../ereuse_devicehub - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/association-events.puml b/docs/association-events.puml new file mode 100644 index 00000000..34ef9f69 --- /dev/null +++ b/docs/association-events.puml @@ -0,0 +1,72 @@ +@startuml +ChangeAssociation <|-- Organize +ChangeAssociation <|-- Transfer +Organize <|-- Plan +Organize <|-- Allocate +Allocate <|-- Accept +Allocate <|-- Reject +Allocate <|-- Assign +Allocate <|-- Authorize +Plan <|-- Reserve +Plan <|-- Cancel +Transfer <|-- Receive +ChangeAssociation <|-- Trade +Trade <|-- Sell +Trade <|-- Donate +Trade <|-- Pay +Trade <|-- Rent +Trade <|-- DisposeProduct + +class ChangeAssociation { + agent: who did it +} + +class Receive { + sender + recipient +} + +class Reserve { + reservee +} + +class Cancel { + reservee +} + +class Trade { + +} + +class Allocate { + purpose +} + +class Sell { + buyer +} + +class Donate { + recipient +} + +class Pay { + purpose + recipient +} + +class Rent { + recipient +} + + +Association <|-- PhysicalPossessor +Association <|-- TradeAssociation +TradeAssociation <|-- Usufructuary +TradeAssociation <|-- Ownership + +Sell - TradeAssociation +Donate - TradeAssociation +Rent -- Usufructuary : Sure? +Receive - PhysicalPossessor +@enduml \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index d8b26965..21a65209 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,41 +1,47 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# DeviceHub documentation build configuration file, created by -# sphinx-quickstart on Mon Apr 18 16:40:20 2016. +# Configuration file for the Sphinx documentation builder. # -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config -import os -import sys +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../ereuse_devicehub')) +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ------------------------------------------------ + +# -- Project information ----------------------------------------------------- + +project = 'Devicehub' +copyright = '2018, eReuse.org team' +author = 'eReuse.org team' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '0.1' + +# -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.4.7' +# +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', - 'sphinxcontrib.httpdomain', - 'sphinx.ext.todo' + 'sphinxcontrib.plantuml' ] # Add any paths that contain templates here, relative to this directory. @@ -43,29 +49,13 @@ templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: +# # source_suffix = ['.rst', '.md'] source_suffix = '.rst' -# The encoding of source files. -# source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' -# General information about the project. -project = 'DeviceHub' -copyright = '2017, eReuse.org team' -author = 'eReuse.org team' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.1' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # @@ -73,157 +63,65 @@ release = '0.1' # Usually you set "language" from the command line for these cases. language = None -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path +# This pattern also affects html_static_path and html_extra_path . exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = False - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. +# html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. +# # html_theme_options = {} -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -# html_title = 'DeviceHub v0.1' - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -# html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# # html_sidebars = {} -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -html_split_index = True - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# html_search_scorer = 'scorer.js' +# -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'DeviceHubdoc' +htmlhelp_basename = 'Devicehubdoc' -# -- Options for LaTeX output --------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). + # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). + # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. + # # 'preamble': '', # Latex figure (float) alignment + # # 'figure_align': 'htbp', } @@ -231,70 +129,44 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'DeviceHub.tex', 'DeviceHub Documentation', + (master_doc, 'Devicehub.tex', 'Devicehub Documentation', 'eReuse.org team', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- +# -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'devicehub', 'DeviceHub Documentation', + (master_doc, 'devicehub', 'Devicehub Documentation', [author], 1) ] -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- +# -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'DeviceHub', 'DeviceHub Documentation', - author, 'DeviceHub', 'One line description of project.', + (master_doc, 'Devicehub', 'Devicehub Documentation', + author, 'Devicehub', 'One line description of project.', 'Miscellaneous'), ] -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False +# -- Extension configuration ------------------------------------------------- +# -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/': None} -autodoc_default_flags = ['members', 'private-members'] -autodoc_member_order = 'bysource' +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# Plantuml +plantuml_output_format = 'svg' + +# favicon +html_favicon = 'img/favicon.ico' diff --git a/docs/event-diagram.rst b/docs/event-diagram.rst new file mode 100644 index 00000000..16bbb925 --- /dev/null +++ b/docs/event-diagram.rst @@ -0,0 +1,5 @@ +Event Diagram +============= + +.. uml:: events.puml + :width: 100% \ No newline at end of file diff --git a/docs/events.puml b/docs/events.puml new file mode 100644 index 00000000..73ec91b2 --- /dev/null +++ b/docs/events.puml @@ -0,0 +1,101 @@ +@startuml +abstract class Rate +abstract class Event +abstract class Test +abstract class Allocate +abstract class Transfer +abstract class Trade +abstract class EventWithOneDevice +abstract class EventWithMultipleDevices +abstract class Organize +abstract class Plan +abstract class Step +abstract class PhotoboxRate + + +package "Devices" { + abstract class Device + abstract class Component + Device <|- Component +} + + +IndividualRate "1..*" -- "1..*" AggregateRate : ratings < + +Event <|-- EventWithOneDevice +Event <|-- EventWithMultipleDevices +EventWithOneDevice <|--- Snapshot +EventWithOneDevice <|--- Install +EventWithOneDevice <|-- Rate +Rate <|-- AggregateRate +Rate <|- IndividualRate +IndividualRate <|- PhotoboxRate +IndividualRate <|-- WorkbenchRate +EventWithOneDevice <|-- Test +Test <|-- TestDataStorage +Test <|-- StressTest +EventWithOneDevice <|--- EraseBasic +EraseBasic <|- EraseSectors + +Step <|-- StepZero +Step <|-- StepRandom +Snapshot "1" -- "1" SnapshotRequest +Event "*" -> "0..1" Snapshot : InSnapshot > +Event "*" -> "0..1" Component : affectedComponents > +Device "1" *-- "*" EventWithOneDevice : EventOn < +Device "1..*" *-- "1" EventWithMultipleDevices : EventOn < +EraseBasic "1" *-- "1..*" Step +PhotoboxRate <|-- PhotoboxSystemRate +PhotoboxRate <|-- PhotoboxPersonRate + +package Images{ + ImageList "1" *- "1..*" Image : In < + Device "1" *-- "*" ImageList + Image "1" *-- "*" PhotoboxRate +} + +EventWithMultipleDevices <|- Organize +EventWithMultipleDevices <|-- Transfer +EventWithMultipleDevices <|-- Trade +EventWithMultipleDevices <|--- ToDispose +EventWithMultipleDevices <|--- Locate +EventWithMultipleDevices <|--- Migrate +EventWithMultipleDevices <|--- Prepare +EventWithMultipleDevices <|--- ReadyToUse +EventWithMultipleDevices <|--- Recycle +EventWithMultipleDevices <|--- Repair +EventWithMultipleDevices <|--- ToPrepare +EventWithMultipleDevices <|--- ToRepair +EventWithMultipleDevices <|--- DisposeWaste +EventWithMultipleDevices <|--- Recover +Transfer <|-- Receive +Trade <|-- Sell +Trade <|-- DisposeProduct +Trade <|-- Donate +Trade <|-- Pay +Trade <|-- Rent +Organize <|-- Allocate +Allocate <|-- Accept +Allocate <|-- Reject +Allocate <|-- Assign +Organize <|-- Plan +Plan <|-- Reserve +Plan <|-- CancelReservation + + +package Agents { + abstract class User + abstract class Agent + Event "*" -> "1" User : Author > + Event "*" - "0..1" Agent : agent > + + Agent <|-- User + + User <|-- Person + User <|-- System + Agent <|-- Organization + User "*" -o "0..1" Organization : WorksIn > + User "*" -o "0..1" Organization : activeOrganization > +} + +@enduml \ No newline at end of file diff --git a/docs/events.rst b/docs/events.rst new file mode 100644 index 00000000..22e881b2 --- /dev/null +++ b/docs/events.rst @@ -0,0 +1,183 @@ +Events +====== + +.. toctree:: +:maxdepth: 4 + + event-diagram + + +Rate +---- +Devicehub generates an rating for a device taking into consideration the +visual, functional, and performance. + +.. todo:: add performance as a result of component fusion + general tests in https:// +github.com/eReuse/Rdevicescore/blob/master/img/input_process_output.png + +A Workflow is as follows: + +1. An agent generates feedback from the device in the form of benchmark, + visual, and functional information; which is filled in a ``Rate`` + event. This is done through a **software**, defining the type + of ``Rate`` event. At the moment we have two rates: ``WorkbenchRate`` + and ``PhotoboxRate``. +2. Devicehub gathers this information and computes a score that updates + the ``Rate`` event. +3. Devicehub aggregates different rates and computes a final score for + the device by performing a new ``AggregateRating`` event. + +There are two **types** of ``Rate``: ``WorkbenchRate`` and +``PhotoboxRate``. Moreover, each rate can have different **versions**, +or different revisions of the algorithm used to compute the final score, +and Devicehub generates a rate event for **each** version. So, if +an agent fulfills a ``WorkbenchRate`` and there are 3 versions, Devicehub +generates 3 ``WorkbenchRate``. Devicehub understands that only one +version is the **official** and it will generate an ``AggregateRating`` +only from the **official** version. + +.. todo:: we should be able to disable a version without destroying code + +In the future, Devicehub will be able to use different and independent +algorithms to calculate a ``Rate`` (not only changed by versions). + +The technical Workflow in Devicehub is as follows: + +1. In **T1**, the user performs a ``Snapshot`` by processing the device + through the Workbench. From the benchmarks and the visual and + functional ratings the user does in the device, the system generates + a ``WorkbenchRate``. With only this information, + the system generates an ``AggregateRating``, which is the event + that the user will see in the web. +2. In **T2**, the user takes pictures from the device through the + Photobox, and DeviceHub crates an ``ImageSet`` with multiple + ``Image`` with information from the photobox. +3. In **T3**, an agent (user or AI) rates the pictures, creating a + ``PhotoboxRate`` **for each** picture. When Devicehub receives the + first ``PhotoboxRate`` it creates an ``AggregateRating`` linked + to such ``PhotoboxRate``. So, the agent will perform as many + ``PhotoboxRate`` as pictures are in the ``ImageSet``, and Devicehub + will link each ``PhotoboxRate`` to the same ``AggregateRating``. + This will end in **T3+Tn**, being *n* the number of photos to rate. +4. In **T3+Tn**, after the last photo is rated, Devicehub will generate + a new rate for the device: it takes the ``AggregateRating`` from 3. + and computes a rate from all the linked ``PhotoboxRate`` plus the + last available ``WorkbenchRate`` for that device. + +If the agent in 3. is an user, Devicehub creates ``PhotoboxUserRate`` +and if it is an AI it creates ``PhotoboxAIRate``. + +The same ``ImageSet`` can be rated multiple times, generating a new +``AggregateRating`` each time. + +.. todo:: which info does photobox provide for each picture? + +Snapshot +-------- +The Snapshot sets the physical information of the device (S/N, model...) +and updates it with erasures, benchmarks, ratings, and tests; updates the +composition of its components (adding / removing them), and links tags +to the device. + +When receiving a Snapshot, the DeviceHub creates, adds and removes +components to match the Snapshot. For example, if a Snapshot of a computer +contains a new component, the system searches for the component in its +database and, if not found, its creates it; finally linking it to the +computer. + +A Snapshot is used with Remove to represent changes in components for +a device: + +1. ``Snapshot`` creates a device if it does not exist, and the same + for its components. This is all done in one ``Snapshot``. +2. If the device exists, it updates its component composition by + *adding* and *removing* them. If, + for example, this new Snasphot doesn't have a component, it means that + this component is not present anymore in the device, thus removing it + from it. Then we have that: + + - Components that are added to the device: snapshot2.components - + snapshot1.components + - Components that are removed to the device: snapshot1.components - + snapshot2.components + + When adding a component, there may be the case this component existed + before and it was inside another device. In such case, DeviceHub will + perform ``Remove`` on the old parent. + +Snapshots from Workbench +~~~~~~~~~~~~~~~~~~~~~~~~ +When processing a device from the Workbench, this one performs a Snapshot +and then performs more events (like testings, benchmarking...). + +The use case, which is represented in the ``test_workbench_phases``, +is as follows: + +1. In **T1**, WorkbenchServer (as the middleware from Workbench and + Devicehub) submits: + + - A ``Snapshot`` event with the required information to **synchronize** + and **rate** the device. This is: + + - Identification information about the device and components + (S/N, model, physical characteristics...) + - Tags. + - Rate. + - Benchmarks. + - TestDataStorage. + - An ordered set of **expected events**, defining which are the next + events that Workbench will perform to the device in ideal + conditions (device doesn't fail, no Internet drop...). + + Devicehub **syncs** the device with the database and perform the + ``Benchmark``, the ``TestDataStorage``, and finally the ``Rate``. + This leaves the Snapshot **open** to wait for the next events + to come. +2. Assuming that we expect all events, in **T2**, WorkbenchServer + submits a ``StressTest`` with a ``snapshot`` field containing the + ID of the Snapshot in 1, and Devicehub links the event with such + ``Snapshot``. +3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot`` + and ``component`` IDs from 1, linking it to them. It repeats + this for all the erased data storage devices; **T2+Tn** being + *n* the erased data storage devices. +4. WorkbenchServer does like in 3. but for the event ``Install``, + finishing in **T2+Tn+Tx**, being *x* the number of data storage + devices with an OS installed into. +5. In **T2+Tn+Tx**, when all *expected events* have been performed, + Devicehub **closes** the ``Snapshot`` from 1. + +Optionally, Devicehub understands receiving a ``Snapshot`` with all +the events the following way: + +- ``Install`` embedded in a ``installation`` field in its respective + ``DataStorage`` component in the ``Snapshot``. +- ``Erase`` embedded in ``erasure`` field in its respective + ``DataStorage`` in the ``Snapshot``. +- ``StressTest`` in an ``events`` field in the ``Snapshot``. + + +ToDispose and DisposeProduct +---------------------------- +There are four events for getting rid of devices: + +- ``ToDispose``: The device is marked to be disposed. +- ``DisposeProduct``: The device has been disposed. This is a ``Trade`` + event, which means that you can optionally ``DisposeProduct`` + to someone. +- ``RecyclingCenter`` have two extra special events: + - ``DisposeWaste``: The device has been disposed in an unspecified + manner. + - ``Recover``: The device has been scrapped and its materials have + been recovered under a new product. + +.. note:: For usability purposes, users might not directly perform +``Dispose``, but this could automatically be done when + performing ``ToDispose`` + ``Receive`` to a + ``RecyclingCenter``. + +.. todo:: Ensure that ``Dispose`` is a ``Trade`` event. An Org could +``Sell`` or ``Donate`` a device with the objective of + disposing them. Is ``Dispose`` ok, or do we want to keep + that extra ``Sell`` or ``Donate`` event? Could dispose + be a synonym of any of those? diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 00000000..35e1e90f Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/index.rst b/docs/index.rst index a8ba6321..faa2b0c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,20 +1,20 @@ -.. Dependencies: sphinx sphinxcontrib-httpdomain -.. title:: DeviceHub +.. title:: DeviceHub .. image:: https://www.ereuse.org/files/2017/04/DeviceHub-logo-V2.svg :height: 100px :alt: DeviceHub logo -This is the documentation and API of the -`eReuse.org DeviceHub `_. + +This is the documentation and API of the `eReuse.org DeviceHub +`_. + .. toctree:: :maxdepth: 4 - snapshot - + events + tags * :ref:`genindex` * :ref:`modindex` - -.. image:: \ No newline at end of file +* :ref:`search` diff --git a/docs/snapshot.rst b/docs/snapshot.rst deleted file mode 100644 index 6c72c2cc..00000000 --- a/docs/snapshot.rst +++ /dev/null @@ -1,23 +0,0 @@ -Snapshot -======== -The Snapshot updates the state of the device with information about its components and events -performed at them. - -When receiving a Snapshot, the DeviceHub creates, adds and removes components to match the -Snapshot. For example, if a Snapshot of a computer contains a new component, the system will -search for the component in its database and, if not found, create it, and finally adding it -to the computer. - -Snapshots can bundle some events, usually tests and hard-drive erasures. In such case the -DeviceHub will save those events. - -A Snapshot is used with Remove to represent changes in components for a device: -1. A device is created in the database always with a Snapshot. If this device had components, - they are created (if they did not existed before) in the same time with the same Snapshot. -2. Time after, a new Snapshot updates component information. If, for example, this new Snasphot - doesn't have a component, it means that this component is not present anymore in the device, - thus removing it from it. Then we have that: - - Components to add: snapshot2.components - snapshot1.components - - Components to remove: snapshot1.components - snapshot2.components - When adding a component, there may be the case this component existed before and it was - inside another device. In such case, DeviceHub will perform ``Remove`` on the old parent. diff --git a/ereuse_devicehub/client.py b/ereuse_devicehub/client.py index b67c71e4..14d9bff5 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -1,11 +1,11 @@ -from typing import Any, Iterable, Tuple, Type, Union +from typing import Any, Dict, Iterable, Tuple, Type, Union, Generator from boltons.typeutils import issubclass -from ereuse_utils.test import JSON from flask import Response from werkzeug.exceptions import HTTPException from ereuse_devicehub.resources.models import Thing +from ereuse_utils.test import JSON from teal.client import Client as TealClient from teal.marshmallow import ValidationError @@ -21,7 +21,7 @@ class Client(TealClient): def open(self, uri: str, - res: str or Type[Thing] = None, + res: Union[str, Type[Thing]] = None, status: Union[int, Type[HTTPException], Type[ValidationError]] = 200, query: Iterable[Tuple[str, Any]] = tuple(), accept=JSON, @@ -29,7 +29,7 @@ class Client(TealClient): item=None, headers: dict = None, token: str = None, - **kw) -> (dict or str, Response): + **kw) -> Tuple[Union[Dict[str, Any], str], Response]: if issubclass(res, Thing): res = res.__name__ return super().open(uri, res, status, query, accept, content_type, item, headers, token, @@ -44,7 +44,7 @@ class Client(TealClient): accept: str = JSON, headers: dict = None, token: str = None, - **kw) -> (dict or str, Response): + **kw) -> Tuple[Union[Dict[str, Any], str], Response]: return super().get(uri, res, query, status, item, accept, headers, token, **kw) def post(self, @@ -57,7 +57,7 @@ class Client(TealClient): accept: str = JSON, headers: dict = None, token: str = None, - **kw) -> (dict or str, Response): + **kw) -> Tuple[Union[Dict[str, Any], str], Response]: return super().post(data, uri, res, query, status, content_type, accept, headers, token, **kw) @@ -66,6 +66,20 @@ class Client(TealClient): assert isinstance(password, str) return self.post({'email': email, 'password': password}, '/users/login', status=200) + def get_many(self, + res: Union[Type[Thing], str], + resources: Iterable[dict], + key: str = None, + headers: dict = None, + token: str = None, + accept: str = JSON, + **kw) -> Iterable[Union[Dict[str, Any], str]]: + """Like :meth:`.get` but with many resources.""" + return ( + self.get(res=res, item=r['key'] if key else r, headers=headers, token=token, **kw)[0] + for r in resources + ) + class UserClient(Client): """ @@ -87,7 +101,7 @@ class UserClient(Client): def open(self, uri: str, - res: str = None, + res: Union[str, Type[Thing]] = None, status: int or HTTPException = 200, query: Iterable[Tuple[str, Any]] = tuple(), accept=JSON, @@ -95,6 +109,6 @@ class UserClient(Client): item=None, headers: dict = None, token: str = None, - **kw) -> (dict or str, Response): + **kw) -> Tuple[Union[Dict[str, Any], str], Response]: return super().open(uri, res, status, query, accept, content_type, item, headers, self.user['token'] if self.user else token, **kw) diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 124d9580..f25d09f1 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -1,27 +1,34 @@ from distutils.version import StrictVersion +from typing import Set -from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DesktopDef, DeviceDef, \ - GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, MotherboardDef, NetbookDef, \ - NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef -from ereuse_devicehub.resources.event import AddDef, EventDef, RemoveDef, SnapshotDef, TestDef, \ - TestHardDriveDef +from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DataStorageDef, \ + DesktopDef, DeviceDef, GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, \ + MotherboardDef, NetbookDef, NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef, \ + SolidStateDriveDef +from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, EventDef, InstallDef, \ + PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \ + StepRandomDef, StepZeroDef, TestDataStorageDef, TestDef, WorkbenchRateDef, EraseBasicDef, \ + EraseSectorsDef from ereuse_devicehub.resources.tag import TagDef from ereuse_devicehub.resources.user import OrganizationDef, UserDef from teal.config import Config class DevicehubConfig(Config): - RESOURCE_DEFINITIONS = ( + RESOURCE_DEFINITIONS = { DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, - MicrotowerDef, ComponentDef, GraphicCardDef, HardDriveDef, MotherboardDef, - NetworkAdapterDef, RamModuleDef, ProcessorDef, UserDef, OrganizationDef, TagDef, EventDef, - AddDef, RemoveDef, SnapshotDef, TestDef, TestHardDriveDef - ) - PASSWORD_SCHEMES = {'pbkdf2_sha256'} - SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' - MIN_WORKBENCH = StrictVersion('11.0') + MicrotowerDef, ComponentDef, GraphicCardDef, DataStorageDef, SolidStateDriveDef, + HardDriveDef, MotherboardDef, NetworkAdapterDef, RamModuleDef, ProcessorDef, UserDef, + OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef, + StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef, + PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef, + TestDataStorageDef, WorkbenchRateDef + } + PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str] + SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' # type: str + MIN_WORKBENCH = StrictVersion('11.0') # type: StrictVersion """ - The minimum version of eReuse.org Workbench that this Devicehub + The minimum algorithm_version of eReuse.org Workbench that this Devicehub accepts. We recommend not changing this value. """ ORGANIZATION_NAME = None # type: str diff --git a/ereuse_devicehub/resources/device/__init__.py b/ereuse_devicehub/resources/device/__init__.py index 7d426c54..737a9329 100644 --- a/ereuse_devicehub/resources/device/__init__.py +++ b/ereuse_devicehub/resources/device/__init__.py @@ -1,6 +1,6 @@ -from ereuse_devicehub.resources.device.schemas import Component, Computer, Desktop, Device, \ - GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, Processor, \ - RamModule, Server +from ereuse_devicehub.resources.device.schemas import Component, Computer, DataStorage, Desktop, \ + Device, GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, \ + Processor, RamModule, Server, SolidStateDrive from ereuse_devicehub.resources.device.views import DeviceView from teal.resource import Converters, Resource @@ -44,10 +44,18 @@ class GraphicCardDef(ComponentDef): SCHEMA = GraphicCard -class HardDriveDef(ComponentDef): +class DataStorageDef(ComponentDef): + SCHEMA = DataStorage + + +class HardDriveDef(DataStorageDef): SCHEMA = HardDrive +class SolidStateDriveDef(DataStorageDef): + SCHEMA = SolidStateDrive + + class MotherboardDef(ComponentDef): SCHEMA = Motherboard diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 76ec3b61..926ea995 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -3,37 +3,38 @@ from itertools import chain from operator import attrgetter from typing import Dict, Set -from ereuse_utils.naming import Naming from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \ Unicode, inspect from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import ColumnProperty, backref, relationship from sqlalchemy.util import OrderedSet +from sqlalchemy_utils import ColorType from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing +from ereuse_utils.naming import Naming from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range class Device(Thing): - id = Column(BigInteger, Sequence('device_seq'), primary_key=True) # type: int + id = Column(BigInteger, Sequence('device_seq'), primary_key=True) type = Column(Unicode(STR_SM_SIZE), nullable=False) - hid = Column(Unicode(STR_BIG_SIZE), unique=True) # type: str - pid = Column(Unicode(STR_SIZE)) # type: str - gid = Column(Unicode(STR_SIZE)) # type: str - model = Column(Unicode(STR_BIG_SIZE)) # type: str - manufacturer = Column(Unicode(STR_SIZE)) # type: str - serial_number = Column(Unicode(STR_SIZE)) # type: str - weight = Column(Float(precision=3, decimal_return_scale=3), - check_range('weight', 0.1, 3)) # type: float - width = Column(Float(precision=3, decimal_return_scale=3), - check_range('width', 0.1, 3)) # type: float - height = Column(Float(precision=3, decimal_return_scale=3), - check_range('height', 0.1, 3)) # type: float + hid = Column(Unicode(STR_BIG_SIZE), unique=True) + model = Column(Unicode(STR_BIG_SIZE)) + manufacturer = Column(Unicode(STR_SIZE)) + serial_number = Column(Unicode(STR_SIZE)) + weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3)) + width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3)) + height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3)) + depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3)) + color = Column(ColorType) @property def events(self) -> list: - """All the events performed to the device.""" - return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('id')) + """ + All the events performed to the device, + ordered by ascending creation time. + """ + return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created')) def __init__(self, *args, **kw) -> None: super().__init__(*args, **kw) @@ -79,7 +80,7 @@ class Device(Thing): class Computer(Device): - id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) # type: int + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) class Desktop(Computer): @@ -103,7 +104,7 @@ class Microtower(Computer): class Component(Device): - id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) # type: int + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) parent_id = Column(BigInteger, ForeignKey(Computer.id)) parent = relationship(Computer, @@ -145,23 +146,31 @@ class JoinedComponentTableMixin: class GraphicCard(JoinedComponentTableMixin, Component): - memory = Column(SmallInteger, check_range('memory', min=1, max=10000)) # type: int + memory = Column(SmallInteger, check_range('memory', min=1, max=10000)) -class HardDrive(JoinedComponentTableMixin, Component): - size = Column(Integer, check_range('size', min=1, max=10 ** 8)) # type: int +class DataStorage(JoinedComponentTableMixin, Component): + size = Column(Integer, check_range('size', min=1, max=10 ** 8)) + + +class HardDrive(DataStorage): + pass + + +class SolidStateDrive(DataStorage): + pass class Motherboard(JoinedComponentTableMixin, Component): - slots = Column(SmallInteger, check_range('slots')) # type: int - usb = Column(SmallInteger, check_range('usb')) # type: int - firewire = Column(SmallInteger, check_range('firewire')) # type: int - serial = Column(SmallInteger, check_range('serial')) # type: int - pcmcia = Column(SmallInteger, check_range('pcmcia')) # type: int + slots = Column(SmallInteger, check_range('slots')) + usb = Column(SmallInteger, check_range('usb')) + firewire = Column(SmallInteger, check_range('firewire')) + serial = Column(SmallInteger, check_range('serial')) + pcmcia = Column(SmallInteger, check_range('pcmcia')) class NetworkAdapter(JoinedComponentTableMixin, Component): - speed = Column(SmallInteger, check_range('speed', min=10, max=10000)) # type: int + speed = Column(SmallInteger, check_range('speed', min=10, max=10000)) class Processor(JoinedComponentTableMixin, Component): diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi new file mode 100644 index 00000000..a27b848d --- /dev/null +++ b/ereuse_devicehub/resources/device/models.pyi @@ -0,0 +1,151 @@ +from typing import Dict, List, Set + +from colour import Color +from sqlalchemy import Column + +from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ + EventWithOneDevice +from ereuse_devicehub.resources.image.models import ImageList +from ereuse_devicehub.resources.models import Thing +from ereuse_devicehub.resources.tag import Tag + + +class Device(Thing): + id = ... # type: Column + type = ... # type: Column + hid = ... # type: Column + model = ... # type: Column + manufacturer = ... # type: Column + serial_number = ... # type: Column + weight = ... # type: Column + width = ... # type: Column + height = ... # type: Column + depth = ... # type: Column + color = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.id = ... # type: int + self.type = ... # type: str + self.hid = ... # type: str + self.model = ... # type: str + self.manufacturer = ... # type: str + self.serial_number = ... # type: str + self.weight = ... # type: float + self.width = ... # type:float + self.height = ... # type: float + self.depth = ... # type: float + self.color = ... # type: Color + self.events = ... # type: List[Event] + self.physical_properties = ... # type: Dict[str, object or None] + self.events_multiple = ... # type: Set[EventWithMultipleDevices] + self.events_one = ... # type: Set[EventWithOneDevice] + self.images = ... # type: ImageList + self.tags = ... # type: Set[Tag] + + +class Computer(Device): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.components = ... # type: Set[Component] + + +class Desktop(Computer): + pass + + +class Laptop(Computer): + pass + + +class Netbook(Computer): + pass + + +class Server(Computer): + pass + + +class Microtower(Computer): + pass + + +class Component(Device): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.parent_id = ... # type: int + self.parent = ... # type: Computer + self.events_components = ... # type: Set[Event] + + def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component': + pass + + +class GraphicCard(Component): + memory = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.memory = ... # type: int + + +class DataStorage(Component): + size = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.size = ... # type: int + + +class HardDrive(DataStorage): + pass + + +class SolidStateDrive(DataStorage): + pass + + +class Motherboard(Component): + slots = ... # type: Column + usb = ... # type: Column + firewire = ... # type: Column + serial = ... # type: Column + pcmcia = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.slots = ... # type: int + self.usb = ... # type: int + self.firewire = ... # type: int + self.serial = ... # type: int + self.pcmcia = ... # type: int + + +class NetworkAdapter(Component): + speed = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.speed = ... # type: int + + +class Processor(Component): + speed = ... # type: Column + cores = ... # type: Column + address = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.speed = ... # type: float + self.cores = ... # type: int + self.address = ... # type: int + + +class RamModule(Component): + size = ... # type: Column + speed = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.size = ... # type: int + self.speed = ... # type: float diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 0f3490f1..cb5dd1e1 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -14,11 +14,6 @@ class Device(Thing): description='The Hardware ID is the unique ID traceability systems ' 'use to ID a device globally.') tags = NestedOn('Tag', many=True, collection_class=OrderedSet) - pid = Str(description='The PID identifies a device under a circuit or platform.', - validate=Length(max=STR_SIZE)) - gid = Str(description='The Giver ID links the device to the giver\'s (donor, seller)' - 'internal inventory.', - validate=Length(max=STR_SIZE)) model = Str(validate=Length(max=STR_BIG_SIZE)) manufacturer = Str(validate=Length(max=STR_SIZE)) serial_number = Str(data_key='serialNumber') @@ -70,7 +65,7 @@ class GraphicCard(Component): description='The amount of memory of the Graphic Card in MB.') -class HardDrive(Component): +class DataStorage(Component): size = Integer(validate=Range(0, 10 ** 8), unit=UnitCodes.mbyte, description='The size of the hard-drive in MB.') @@ -79,6 +74,14 @@ class HardDrive(Component): benchmarks = NestedOn('BenchmarkHardDrive', load_only=True, many=True) +class HardDrive(DataStorage): + pass + + +class SolidStateDrive(DataStorage): + pass + + class Motherboard(Component): slots = Integer(validate=Range(1, 20), description='PCI slots the motherboard has.') usb = Integer(validate=Range(0, 20)) diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 8de4dac2..daaec583 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -47,13 +47,14 @@ class Sync: :return: A tuple of: 1. The device from the database (with an ID) whose - ``components`` field contain the db version + ``components`` field contain the db algorithm_version of the passed-in components. 2. A list of Add / Remove (not yet added to session). """ db_device = self.execute_register(device) db_components, events = OrderedSet(), OrderedSet() if components is not None: # We have component info (see above) + assert isinstance(db_device, Computer) blacklist = set() # type: Set[int] not_new_components = set() for component in components: @@ -122,7 +123,7 @@ class Sync: This method tries to get an existing device using the HID or one of the tags, and... - - if it already exists it returns a "local synced version" + - if it already exists it returns a "local synced algorithm_version" –the same ``device`` you passed-in but with updated values from the database. In this case we do not "touch" any of its values on the DB. @@ -187,7 +188,7 @@ class Sync: setattr(db_device, field_name, value) @staticmethod - def add_remove(device: Device, + def add_remove(device: Computer, components: Set[Component]) -> OrderedSet: """ Generates the Add and Remove events (but doesn't add them to @@ -209,7 +210,7 @@ class Sync: adding = components - old_components if adding: # For the components we are adding, let's remove them from their old parents - def g_parent(component: Component) -> int: + def g_parent(component: Component) -> Device: return component.parent or Computer(id=0) # Computer with id 0 is our Identity for parent, _components in groupby(sorted(adding, key=g_parent), key=g_parent): diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py new file mode 100644 index 00000000..9803077c --- /dev/null +++ b/ereuse_devicehub/resources/enums.py @@ -0,0 +1,127 @@ +from distutils.version import StrictVersion +from enum import Enum, IntEnum, unique +from typing import Union + + + +@unique +class SnapshotSoftware(Enum): + """The algorithm_software used to perform the Snapshot.""" + Workbench = 'Workbench' + AndroidApp = 'AndroidApp' + Web = 'Web' + DesktopApp = 'DesktopApp' + + +@unique +class RatingSoftware(Enum): + """The algorithm_software used to compute the Score.""" + Ereuse = 'Ereuse' + + +RATE_POSITIVE = 0, 10 +RATE_NEGATIVE = -3, 5 + + +@unique +class RatingRange(IntEnum): + """ + The human translation to score range. + + You can compare them: ScoreRange.VERY_LOW < ScoreRange.LOW + """ + VERY_LOW = 2 + LOW = 3 + MEDIUM = 4 + HIGH = 5 + + @classmethod + def from_score(cls, val: Union[int, float]) -> 'RatingRange': + assert 0 <= val <= 10, 'Value is not a valid score.' + + if val <= cls.VERY_LOW: + return cls.VERY_LOW + elif val <= cls.LOW: + return cls.LOW + elif val <= cls.MEDIUM: + return cls.MEDIUM + else: + return cls.HIGH + + +@unique +class AggregateRatingVersions(Enum): + v1 = StrictVersion('1.0') + """ + This algorithm_version is set to aggregate :class:`ereuse_devicehub.resources. + event.models.WorkbenchRate` algorithm_version X and :class:`ereuse_devicehub. + resources.event.models.PhotoboxRate` algorithm_version Y. + """ + + +@unique +class AppearanceRange(Enum): + """Grades the imperfections that aesthetically affect the device, but not its usage.""" + Z = '0. The device is new.' + A = 'A. Is like new (without visual damage)' + B = 'B. Is in really good condition (small visual damage in difficult places to spot)' + C = 'C. Is in good condition (small visual damage in parts that are easy to spot, not screens)' + D = 'D. Is acceptable (visual damage in visible parts, not screens)' + E = 'E. Is unacceptable (considerable visual damage that can affect usage)' + + +@unique +class FunctionalityRange(Enum): + """Grades the defects of a device that affect its usage.""" + # todo sync with https://github.com/ereuse/rdevicescore#input + A = 'A. Everything works perfectly (buttons, and in case of screens there are no scratches)' + B = 'B. There is a button difficult to press or a small scratch in an edge of a screen' + C = 'C. A non-important button (or similar) doesn\'t work; screen has multiple scratches in edges' + D = 'D. Multiple buttons don\'t work; screen has visual damage resulting in uncomfortable usage' + + +@unique +class Bios(Enum): + """How difficult it has been to set the bios to boot from the network.""" + A = 'A. If by pressing a key you could access a boot menu with the network boot' + B = 'B. You had to get into the BIOS, and in less than 5 steps you could set the network boot' + C = 'C. Like B, but with more than 5 steps' + D = 'D. Like B or C, but you had to unlock the BIOS (i.e. by removing the battery)' + E = 'E. The device could not be booted through the network.' + + +@unique +class Orientation(Enum): + Vertical = 'vertical' + Horizontal = 'Horizontal' + + +@unique +class TestHardDriveLength(Enum): + Short = 'Short' + Extended = 'Extended' + + +@unique +class ImageSoftware(Enum): + Photobox = 'Photobox' + + +@unique +class ImageMimeTypes(Enum): + """Supported image Mimetypes for Devicehub.""" + jpg = 'image/jpeg' + png = 'image/png' + + +@unique +class SnapshotExpectedEvents(Enum): + """Events that Workbench can perform when processing a device.""" + TestDataStorage = 'TestDataStorage' + StressTest = 'StressTest' + EraseSectors = 'EraseSectors' + Install = 'Install' + + +BOX_RATE_5 = 1, 5 +BOX_RATE_3 = 1, 3 diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py index 02736217..eb9cd795 100644 --- a/ereuse_devicehub/resources/event/__init__.py +++ b/ereuse_devicehub/resources/event/__init__.py @@ -1,8 +1,9 @@ from typing import Callable, Iterable, Tuple from ereuse_devicehub.resources.device.sync import Sync -from ereuse_devicehub.resources.event.schemas import Add, Event, Remove, Snapshot, Test, \ - TestHardDrive +from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, EraseBasic, Event, \ + Install, PhotoboxSystemRate, PhotoboxUserRate, Rate, Remove, Snapshot, Step, StepRandom, \ + StepZero, Test, TestDataStorage, WorkbenchRate, EraseSectors from ereuse_devicehub.resources.event.views import EventView, SnapshotView from teal.resource import Converters, Resource @@ -22,6 +23,50 @@ class RemoveDef(EventDef): SCHEMA = Remove +class EraseBasicDef(EventDef): + SCHEMA = EraseBasic + + +class EraseSectorsDef(EraseBasicDef): + SCHEMA = EraseSectors + + +class StepDef(Resource): + SCHEMA = Step + + +class StepZeroDef(StepDef): + SCHEMA = StepZero + + +class StepRandomDef(StepDef): + SCHEMA = StepRandom + + +class RateDef(EventDef): + SCHEMA = Rate + + +class AggregateRateDef(RateDef): + SCHEMA = AggregateRate + + +class WorkbenchRateDef(RateDef): + SCHEMA = WorkbenchRate + + +class PhotoboxUserDef(RateDef): + SCHEMA = PhotoboxUserRate + + +class PhotoboxSystemRateDef(RateDef): + SCHEMA = PhotoboxSystemRate + + +class InstallDef(EventDef): + SCHEMA = Install + + class SnapshotDef(EventDef): SCHEMA = Snapshot VIEW = SnapshotView @@ -38,5 +83,5 @@ class TestDef(EventDef): SCHEMA = Test -class TestHardDriveDef(TestDef): - SCHEMA = TestHardDrive +class TestDataStorageDef(TestDef): + SCHEMA = TestDataStorage diff --git a/ereuse_devicehub/resources/event/enums.py b/ereuse_devicehub/resources/event/enums.py deleted file mode 100644 index b12c19bd..00000000 --- a/ereuse_devicehub/resources/event/enums.py +++ /dev/null @@ -1,51 +0,0 @@ -from enum import Enum - - -class StepTypes(Enum): - Zeros = 1 - Random = 2 - - -class SoftwareType(Enum): - """The software used to perform the Snapshot.""" - Workbench = 'Workbench' - AndroidApp = 'AndroidApp' - Web = 'Web' - DesktopApp = 'DesktopApp' - - -class Appearance(Enum): - """Grades the imperfections that aesthetically affect the device, but not its usage.""" - Z = '0. The device is new.' - A = 'A. Is like new (without visual damage)' - B = 'B. Is in really good condition (small visual damage in difficult places to spot)' - C = 'C. Is in good condition (small visual damage in parts that are easy to spot, not screens)' - D = 'D. Is acceptable (visual damage in visible parts, not screens)' - E = 'E. Is unacceptable (considerable visual damage that can affect usage)' - - -class Functionality(Enum): - """Grades the defects of a device that affect its usage.""" - A = 'A. Everything works perfectly (buttons, and in case of screens there are no scratches)' - B = 'B. There is a button difficult to press or a small scratch in an edge of a screen' - C = 'C. A non-important button (or similar) doesn\'t work; screen has multiple scratches in edges' - D = 'D. Multiple buttons don\'t work; screen has visual damage resulting in uncomfortable usage' - - -class Bios(Enum): - """How difficult it has been to set the bios to boot from the network.""" - A = 'A. If by pressing a key you could access a boot menu with the network boot' - B = 'B. You had to get into the BIOS, and in less than 5 steps you could set the network boot' - C = 'C. Like B, but with more than 5 steps' - D = 'D. Like B or C, but you had to unlock the BIOS (i.e. by removing the battery)' - E = 'E. The device could not be booted through the network.' - - -class Orientation(Enum): - Vertical = 'vertical' - Horizontal = 'Horizontal' - - -class TestHardDriveLength(Enum): - Short = 'Short' - Extended = 'Extended' diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index ff79e861..05813919 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -1,48 +1,55 @@ -from datetime import timedelta +from uuid import uuid4 -from colour import Color from flask import g from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \ - ForeignKey, Integer, Interval, JSON, Sequence, SmallInteger, Unicode + Float, ForeignKey, Interval, JSON, SmallInteger, Unicode, event from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import backref, relationship, validates +from sqlalchemy.ext.orderinglist import ordering_list +from sqlalchemy.orm import backref, relationship from sqlalchemy.util import OrderedSet -from sqlalchemy_utils import ColorType from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Component, Device -from ereuse_devicehub.resources.event.enums import Appearance, Bios, Functionality, Orientation, \ - SoftwareType, StepTypes, TestHardDriveLength +from ereuse_devicehub.resources.device.models import Component, DataStorage, Device +from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \ + FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \ + SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength +from ereuse_devicehub.resources.image.models import Image from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing from ereuse_devicehub.resources.user.models import User -from teal.db import CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, \ - StrictVersionType, check_range +from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \ + POLYMORPHIC_ON, StrictVersionType, check_range class JoinedTableMixin: + # noinspection PyMethodParameters @declared_attr def id(cls): - return Column(BigInteger, ForeignKey(Event.id), primary_key=True) + return Column(UUID(as_uuid=True), ForeignKey(Event.id), primary_key=True) class Event(Thing): - id = Column(BigInteger, Sequence('event_seq'), primary_key=True) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) title = Column(Unicode(STR_BIG_SIZE), default='', nullable=False) - date = Column(DateTime) - secured = Column(Boolean, default=False, nullable=False) type = Column(Unicode) incidence = Column(Boolean, default=False, nullable=False) + closed = Column(Boolean, default=True, nullable=False) + """ + Whether the author has finished the event. + After this is set to True, no modifications are allowed. + """ + error = Column(Boolean, default=False, nullable=False) description = Column(Unicode, default='', nullable=False) + date = Column(DateTime) - snapshot_id = Column(BigInteger, ForeignKey('snapshot.id', - use_alter=True, - name='snapshot_events')) + snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id', + use_alter=True, + name='snapshot_events')) snapshot = relationship('Snapshot', backref=backref('events', lazy=True, - cascade=CASCADE, - collection_class=OrderedSet), + cascade=CASCADE_OWN, + collection_class=set), primaryjoin='Event.snapshot_id == Snapshot.id') author_id = Column(UUID(as_uuid=True), @@ -55,12 +62,23 @@ class Event(Thing): components = relationship(Component, backref=backref('events_components', lazy=True, - order_by=lambda: Event.id, + order_by=lambda: Event.created, collection_class=OrderedSet), secondary=lambda: EventComponent.__table__, - order_by=lambda: Device.id, + order_by=lambda: Component.id, collection_class=OrderedSet) + """ + The components that are affected by the event. + + When performing events to parent devices their components are + affected too. + + For example: an ``Allocate`` is performed to a Computer and this + relationship is filled with the components the computer had + at the time of the event. + """ + # noinspection PyMethodParameters @declared_attr def __mapper_args__(cls): """ @@ -79,8 +97,8 @@ class Event(Thing): class EventComponent(db.Model): - device_id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) - event_id = Column(BigInteger, ForeignKey(Event.id), primary_key=True) + device_id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) + event_id = Column(UUID(as_uuid=True), ForeignKey(Event.id), primary_key=True) class EventWithOneDevice(Event): @@ -89,22 +107,19 @@ class EventWithOneDevice(Event): backref=backref('events_one', lazy=True, cascade=CASCADE, - order_by=lambda: EventWithOneDevice.id, + order_by=lambda: EventWithOneDevice.created, collection_class=OrderedSet), primaryjoin=Device.id == device_id) def __repr__(self) -> str: - return '<{0.t} {0.id!r} device={0.device!r}>'.format(self) + return '<{0.t} {0.id!r} device={0.device_id}>'.format(self) class EventWithMultipleDevices(Event): - """ - Note that these events are not deleted when a device is deleted. - """ devices = relationship(Device, backref=backref('events_multiple', lazy=True, - order_by=lambda: EventWithMultipleDevices.id, + order_by=lambda: EventWithMultipleDevices.created, collection_class=OrderedSet), secondary=lambda: EventDevice.__table__, order_by=lambda: Device.id) @@ -115,7 +130,8 @@ class EventWithMultipleDevices(Event): class EventDevice(db.Model): device_id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) - event_id = Column(BigInteger, ForeignKey(EventWithMultipleDevices.id), primary_key=True) + event_id = Column(UUID(as_uuid=True), ForeignKey(EventWithMultipleDevices.id), + primary_key=True) class Add(EventWithOneDevice): @@ -139,11 +155,11 @@ class Deallocate(JoinedTableMixin, EventWithMultipleDevices): class EraseBasic(JoinedTableMixin, EventWithOneDevice): - starting_time = Column(DateTime, nullable=False) - ending_time = Column(DateTime, nullable=False) - secure_random_steps = Column(SmallInteger, check_range('secure_random_steps', min=0), + start_time = Column(DateTime, nullable=False) + end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False) + secure_random_steps = Column(SmallInteger, + check_range('secure_random_steps', min=0), nullable=False) - success = Column(Boolean, nullable=False) clean_with_zeros = Column(Boolean, nullable=False) @@ -152,54 +168,63 @@ class EraseSectors(EraseBasic): class Step(db.Model): - id = Column(BigInteger, Sequence('step_seq'), primary_key=True) - num = Column(SmallInteger, nullable=False) - type = Column(DBEnum(StepTypes), nullable=False) - success = Column(Boolean, nullable=False) - starting_time = Column(DateTime, nullable=False) - ending_time = Column(DateTime, CheckConstraint('ending_time > starting_time'), nullable=False) - secure_random_steps = Column(SmallInteger, check_range('secure_random_steps', min=0), + erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True) + type = Column(Unicode(STR_SM_SIZE), nullable=False) + num = Column(SmallInteger, primary_key=True) + error = Column(Boolean, default=False, nullable=False) + start_time = Column(DateTime, nullable=False) + end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False) + secure_random_steps = Column(SmallInteger, + check_range('secure_random_steps', min=0), nullable=False) clean_with_zeros = Column(Boolean, nullable=False) - erasure_id = Column(BigInteger, ForeignKey(EraseBasic.id)) - erasure = relationship(EraseBasic, backref=backref('steps', cascade=CASCADE_OWN)) + erasure = relationship(EraseBasic, + backref=backref('steps', + cascade=CASCADE_OWN, + order_by=num, + collection_class=ordering_list('num'))) + + # noinspection PyMethodParameters + @declared_attr + def __mapper_args__(cls): + """ + Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.t} + if cls.t == 'Step': + args[POLYMORPHIC_ON] = cls.type + return args + + +class StepZero(Step): + pass + + +class StepRandom(Step): + pass class Snapshot(JoinedTableMixin, EventWithOneDevice): - uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) # type: UUID - version = Column(StrictVersionType(STR_SM_SIZE), nullable=False) # type: str - software = Column(DBEnum(SoftwareType), nullable=False) # type: SoftwareType - appearance = Column(DBEnum(Appearance)) # type: Appearance - appearance_score = Column(SmallInteger, - check_range('appearance_score', -3, 5)) # type: int - functionality = Column(DBEnum(Functionality)) # type: Functionality - functionality_score = Column(SmallInteger, - check_range('functionality_score', min=-3, max=5)) # type: int - labelling = Column(Boolean) # type: bool - bios = Column(DBEnum(Bios)) # type: Bios - condition = Column(SmallInteger, - check_range('condition', min=0, max=5)) # type: int - elapsed = Column(Interval, nullable=False) # type: timedelta - install_name = Column(Unicode(STR_BIG_SIZE)) # type: str - install_elapsed = Column(Interval) # type: timedelta - install_success = Column(Boolean) # type: bool - inventory_elapsed = Column(Interval) # type: timedelta - color = Column(ColorType) # type: Color - orientation = Column(DBEnum(Orientation)) # type: Orientation + uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) + version = Column(StrictVersionType(STR_SM_SIZE), nullable=False) + software = Column(DBEnum(SnapshotSoftware), nullable=False) + elapsed = Column(Interval, nullable=False) + expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents))) - @validates('components') - def validate_components_only_workbench(self, _, components): - if self.software != SoftwareType.Workbench: - if components: - raise ValueError('Only Snapshots from Workbench can have components.') - return components + +class Install(JoinedTableMixin, EventWithOneDevice): + name = Column(Unicode(STR_BIG_SIZE), nullable=False) + elapsed = Column(Interval, nullable=False) class SnapshotRequest(db.Model): - id = Column(BigInteger, ForeignKey(Snapshot.id), primary_key=True) + id = Column(UUID(as_uuid=True), ForeignKey(Snapshot.id), primary_key=True) request = Column(JSON, nullable=False) - snapshot = relationship(Snapshot, backref=backref('request', lazy=True, @@ -207,25 +232,132 @@ class SnapshotRequest(db.Model): cascade=CASCADE_OWN)) +class Rate(JoinedTableMixin, EventWithOneDevice): + rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE)) + algorithm_software = Column(DBEnum(RatingSoftware), nullable=False) + algorithm_version = Column(StrictVersionType, nullable=False) + appearance = Column(Float(decimal_return_scale=2), check_range('appearance', *RATE_NEGATIVE)) + functionality = Column(Float(decimal_return_scale=2), + check_range('functionality', *RATE_NEGATIVE)) + + @property + def rating_range(self) -> RatingRange: + return RatingRange.from_score(self.rating) + + +class IndividualRate(Rate): + pass + + +class AggregateRate(Rate): + id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) + ratings = relationship(IndividualRate, + backref=backref('aggregated_ratings', + lazy=True, + order_by=lambda: IndividualRate.created, + collection_class=OrderedSet), + secondary=lambda: RateAggregateRate.__table__, + order_by=lambda: IndividualRate.created, + collection_class=OrderedSet) + """The ratings this aggregateRate aggregates.""" + + +class RateAggregateRate(db.Model): + """ + Represents the ``many to many`` relationship between + ``Rate`` and ``AggregateRate``. + """ + rate_id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) + aggregate_rate_id = Column(UUID(as_uuid=True), + ForeignKey(AggregateRate.id), + primary_key=True) + + +class WorkbenchRate(IndividualRate): + id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) + processor = Column(Float(decimal_return_scale=2), check_range('processor', *RATE_POSITIVE)) + ram = Column(Float(decimal_return_scale=2), check_range('ram', *RATE_POSITIVE)) + data_storage = Column(Float(decimal_return_scale=2), + check_range('data_storage', *RATE_POSITIVE)) + graphic_card = Column(Float(decimal_return_scale=2), + check_range('graphic_card', *RATE_POSITIVE)) + labelling = Column(Boolean) + bios = Column(DBEnum(Bios)) + appearance_range = Column(DBEnum(AppearanceRange)) + functionality_range = Column(DBEnum(FunctionalityRange)) + + +class PhotoboxRate(IndividualRate): + id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) + image_id = Column(UUID(as_uuid=True), ForeignKey(Image.id), nullable=False) + image = relationship(Image, + uselist=False, + cascade=CASCADE_OWN, + single_parent=True, + primaryjoin=Image.id == image_id) + + # todo how to ensure phtoboxrate.device == image.image_list.device? + + +class PhotoboxUserRate(PhotoboxRate): + id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True) + assembling = Column(SmallInteger, check_range('assembling', *BOX_RATE_5), nullable=False) + parts = Column(SmallInteger, check_range('parts', *BOX_RATE_5), nullable=False) + buttons = Column(SmallInteger, check_range('buttons', *BOX_RATE_5), nullable=False) + dents = Column(SmallInteger, check_range('dents', *BOX_RATE_5), nullable=False) + decolorization = Column(SmallInteger, + check_range('decolorization', *BOX_RATE_5), + nullable=False) + scratches = Column(SmallInteger, check_range('scratches', *BOX_RATE_5), nullable=False) + tag_alignment = Column(SmallInteger, + check_range('tag_alignment', *BOX_RATE_3), + nullable=False) + tag_adhesive = Column(SmallInteger, check_range('tag_adhesive', *BOX_RATE_3), nullable=False) + dirt = Column(SmallInteger, check_range('dirt', *BOX_RATE_3), nullable=False) + + +class PhotoboxSystemRate(PhotoboxRate): + id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True) + + class Test(JoinedTableMixin, EventWithOneDevice): elapsed = Column(Interval, nullable=False) - success = Column(Boolean, nullable=False) - - snapshot = relationship(Snapshot, backref=backref('tests', - lazy=True, - cascade=CASCADE_OWN, - order_by=Event.id, - collection_class=OrderedSet)) -class TestHardDrive(Test): - id = Column(BigInteger, ForeignKey(Test.id), primary_key=True) +class TestDataStorage(Test): + id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True) length = Column(DBEnum(TestHardDriveLength), nullable=False) # todo from type status = Column(Unicode(STR_SIZE), nullable=False) lifetime = Column(Interval, nullable=False) - first_error = Column(Integer) - # todo error becomes Test.success + first_error = Column(SmallInteger, nullable=False, default=0) + passed_lifetime = Column(Interval) + assessment = Column(Boolean) + reallocated_sector_count = Column(SmallInteger) + power_cycle_count = Column(SmallInteger) + reported_uncorrectable_errors = Column(SmallInteger) + command_timeout = Column(SmallInteger) + current_pending_sector_count = Column(SmallInteger) + offline_uncorrectable = Column(SmallInteger) + remaining_lifetime_percentage = Column(SmallInteger) + + # todo remove lifetime / passed_lifetime as I think they are the same class StressTest(Test): pass + + +# Listeners +@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True) +@event.listens_for(Install.device, 'set', retval=True, propagate=True) +@event.listens_for(EraseBasic.device, 'set', retval=True, propagate=True) +def validate_device_is_data_storage(target, value, old_value, initiator): + if not isinstance(value, DataStorage): + raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value)) + return value + +# todo finish adding events +# @event.listens_for(Install.snapshot, 'before_insert', propagate=True) +# def validate_required_snapshot(mapper, connection, target: Event): +# if not target.snapshot: +# raise ValidationError('{0!r} must be linked to a Snapshot.'.format(target)) diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi new file mode 100644 index 00000000..4fff2daf --- /dev/null +++ b/ereuse_devicehub/resources/event/models.pyi @@ -0,0 +1,214 @@ +from datetime import datetime, timedelta +from distutils.version import StrictVersion +from typing import List, Set +from uuid import UUID + +from sqlalchemy import Column +from sqlalchemy.orm import relationship + +from ereuse_devicehub.resources.device.models import Component, Computer, Device +from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ + RatingSoftware, SnapshotSoftware, TestHardDriveLength, SnapshotExpectedEvents +from ereuse_devicehub.resources.image.models import Image +from ereuse_devicehub.resources.models import Thing +from ereuse_devicehub.resources.user import User +from teal.db import Model + + +class Event(Thing): + id = ... # type: Column + title = ... # type: Column + date = ... # type: Column + type = ... # type: Column + incidence = ... # type: Column + description = ... # type: Column + finalized = ... # type: Column + snapshot_id = ... # type: Column + snapshot = ... # type: relationship + author_id = ... # type: Column + author = ... # type: relationship + components = ... # type: relationship + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.id = ... # type: UUID + self.title = ... # type: str + self.type = ... # type: str + self.incidence = ... # type: bool + self.closed = ... # type: bool + self.error = ... # type: bool + self.description = ... # type: str + self.date = ... # type: datetime + self.snapshot_id = ... # type: UUID + self.snapshot = ... # type: Snapshot + self.author_id = ... # type: UUID + self.author = ... # type: User + self.components = ... # type: Set[Component] + + +class EventWithOneDevice(Event): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.device_id = ... # type: int + self.device = ... # type: Device + + +class EventWithMultipleDevices(Event): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.devices = ... # type: Set[Device] + + +class Step(Model): + def __init__(self, **kwargs) -> None: + self.erasure_id = ... # type: UUID + self.type = ... # type: str + self.num = ... # type: int + self.success = ... # type: bool + self.start_time = ... # type: datetime + self.end_time = ... # type: datetime + self.secure_random_steps = ... # type: int + self.clean_with_zeros = ... # type: bool + self.erasure = ... # type: EraseBasic + + +class StepZero(Step): + pass + + +class StepRandom(Step): + pass + + +class Snapshot(EventWithOneDevice): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.uuid = ... # type: UUID + self.version = ... # type: StrictVersion + self.software = ... # type: SnapshotSoftware + self.elapsed = ... # type: timedelta + self.device = ... # type: Computer + self.events = ... # type: Set[Event] + self.expected_events = ... # type: List[SnapshotExpectedEvents] + + +class Install(EventWithOneDevice): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.name = ... # type: str + self.elapsed = ... # type: timedelta + self.success = ... # type: bool + + +class SnapshotRequest(Model): + def __init__(self, **kwargs) -> None: + self.id = ... # type: UUID + self.request = ... # type: dict + self.snapshot = ... # type: Snapshot + + +class Rate(EventWithOneDevice): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.rating = ... # type: float + self.algorithm_software = ... # type: RatingSoftware + self.algorithm_version = ... # type: StrictVersion + self.appearance = ... # type: float + self.functionality = ... # type: float + self.rating_range = ... # type: str + + +class IndividualRate(Rate): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.aggregated_ratings = ... # type: Set[AggregateRate] + + +class AggregateRate(Rate): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.ratings = ... # type: Set[IndividualRate] + + +class WorkbenchRate(IndividualRate): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.processor = ... # type: float + self.ram = ... # type: float + self.data_storage = ... # type: float + self.graphic_card = ... # type: float + self.labelling = ... # type: bool + self.bios = ... # type: Bios + self.appearance_range = ... # type: AppearanceRange + self.functionality_range = ... # type: FunctionalityRange + + +class PhotoboxRate(IndividualRate): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.num = ... # type: int + self.image_id = ... # type: UUID + self.image = ... # type: Image + + +class PhotoboxUserRate(PhotoboxRate): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.assembling = ... # type: int + self.parts = ... # type: int + self.buttons = ... # type: int + self.dents = ... # type: int + self.decolorization = ... # type: int + self.scratches = ... # type: int + self.tag_adhesive = ... # type: int + self.dirt = ... # type: int + + +class PhotoboxSystemRate(PhotoboxRate): + pass + + +class Test(EventWithOneDevice): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.elapsed = ... # type: timedelta + self.success = ... # type: bool + + +class TestDataStorage(Test): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.id = ... # type: UUID + self.length = ... # type: TestHardDriveLength + self.status = ... # type: str + self.lifetime = ... # type: timedelta + self.first_error = ... # type: int + self.passed_lifetime = ... # type: timedelta + self.assessment = ... # type: int + self.reallocated_sector_count = ... # type: int + self.power_cycle_count = ... # type: int + self.reported_uncorrectable_errors = ... # type: int + self.command_timeout = ... # type: int + self.current_pending_sector_count = ... # type: int + self.offline_uncorrectable = ... # type: int + self.remaining_lifetime_percentage = ... # type: int + + +class StressTest(Test): + pass + + +class EraseBasic(EventWithOneDevice): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.start_time = ... # type: datetime + self.end_time = ... # type: datetime + self.secure_random_steps = ... # type: int + self.steps = ... # type: List[Step] + self.clean_with_zeros = ... # type: bool + self.success = ... # type: bool + + +class EraseSectors(EraseBasic): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 868692b0..a2071011 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -1,17 +1,17 @@ from flask import current_app as app -from marshmallow import ValidationError, post_load, validates_schema -from marshmallow.fields import Boolean, DateTime, Integer, Nested, String, TimeDelta, UUID +from marshmallow import ValidationError, validates_schema +from marshmallow.fields import Boolean, DateTime, Float, Integer, Nested, String, TimeDelta, UUID from marshmallow.validate import Length, Range from marshmallow_enum import EnumField from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.device.schemas import Component, Device -from ereuse_devicehub.resources.event.enums import Appearance, Bios, Functionality, Orientation, \ - SoftwareType, StepTypes, TestHardDriveLength +from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ + RATE_POSITIVE, RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.user.schemas import User -from teal.marshmallow import Color, Version +from teal.marshmallow import Version from teal.resource import Schema @@ -23,16 +23,12 @@ class Event(Thing): date = DateTime('iso', description='When this event happened. ' 'Leave it blank if it is happening now. ' 'This is used when creating events retroactively.') - secured = Boolean(default=False, - description='Can we ensure the info in this event is totally correct?' - 'Devicehub will automatically set this too for some events,' - 'for example in snapshots if it could detect the ids of the' - 'hardware without margin of doubt.') + error = Boolean(default=False, description='Did the event fail?') incidence = Boolean(default=False, - description='Was something wrong in this event?') + description='Should this event be reviewed due some anomaly?') snapshot = NestedOn('Snapshot', dump_only=True) - description = String(default='', description='A comment about the event.') components = NestedOn(Component, dump_only=True, many=True) + description = String(default='', description='A comment about the event.') class EventWithOneDevice(Event): @@ -67,12 +63,12 @@ class Deallocate(EventWithMultipleDevices): class EraseBasic(EventWithOneDevice): - starting_time = DateTime(required=True, data_key='startingTime') - ending_time = DateTime(required=True, data_key='endingTime') + start_time = DateTime(required=True, data_key='startTime') + end_time = DateTime(required=True, data_key='endTime') secure_random_steps = Integer(validate=Range(min=0), required=True, data_key='secureRandomSteps') - success = Boolean(required=True) clean_with_zeros = Boolean(required=True, data_key='cleanWithZeros') + steps = NestedOn('Step', many=True, required=True) class EraseSectors(EraseBasic): @@ -81,46 +77,94 @@ class EraseSectors(EraseBasic): class Step(Schema): id = Integer(dump_only=True) - type = EnumField(StepTypes, required=True) - starting_time = DateTime(required=True, data_key='startingTime') - ending_time = DateTime(required=True, data_key='endingTime') + type = String(description='Only required when it is nested.') + start_time = DateTime(required=True, data_key='startTime') + end_time = DateTime(required=True, data_key='endTime') secure_random_steps = Integer(validate=Range(min=0), required=True, data_key='secureRandomSteps') - success = Boolean(required=True) clean_with_zeros = Boolean(required=True, data_key='cleanWithZeros') + error = Boolean(default=False, description='Did the event fail?') -class Condition(Schema): - appearance = EnumField(Appearance, - required=True, - description='Grades the imperfections that aesthetically ' - 'affect the device, but not its usage.') - appearance_score = Integer(validate=Range(-3, 5), dump_only=True) - functionality = EnumField(Functionality, - required=True, - description='Grades the defects of a device that affect its usage.') - functionality_score = Integer(validate=Range(-3, 5), - dump_only=True, - data_key='functionalityScore') +class StepZero(Step): + pass + + +class StepRandom(Step): + pass + + +class Rate(EventWithOneDevice): + rating = Integer(validate=Range(*RATE_POSITIVE), + dump_only=True, + data_key='ratingValue', + description='The rating for the content.') + algorithm_software = EnumField(RatingSoftware, + dump_only=True, + data_key='algorithmSoftware', + description='The algorithm used to produce this rating.') + algorithm_version = Version(dump_only=True, + data_key='algorithmVersion', + description='The algorithm_version of the algorithm_software.') + appearance = Integer(validate=Range(-3, 5), dump_only=True) + functionality = Integer(validate=Range(-3, 5), + dump_only=True, + data_key='functionalityScore') + + +class IndividualRate(Rate): + pass + + +class AggregateRate(Rate): + ratings = NestedOn(IndividualRate, many=True) + + +class PhotoboxRate(IndividualRate): + num = Integer(dump_only=True) + # todo Image + + +class PhotoboxUserRate(PhotoboxRate): + assembling = Integer() + parts = Integer() + buttons = Integer() + dents = Integer() + decolorization = Integer() + scratches = Integer() + tag_adhesive = Integer() + dirt = Integer() + + +class PhotoboxSystemRate(PhotoboxRate): + pass + + +class WorkbenchRate(IndividualRate): + processor = Float() + ram = Float() + data_storage = Float() + graphic_card = Float() labelling = Boolean(description='Sets if there are labels stuck that should be removed.') bios = EnumField(Bios, description='How difficult it has been to set the bios to ' 'boot from the network.') - general = Integer(dump_only=True, - validate=Range(0, 5), - description='The grade of the device.') + appearance_range = EnumField(AppearanceRange, + required=True, + data_key='appearanceRange', + description='Grades the imperfections that aesthetically ' + 'affect the device, but not its usage.') + functionality_range = EnumField(FunctionalityRange, + required=True, + data_key='functionalityRange', + description='Grades the defects of a device that affect its usage.') -class Installation(Schema): +class Install(EventWithOneDevice): name = String(validate=Length(STR_BIG_SIZE), required=True, description='The name of the OS installed.') elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) - success = Boolean(required=True) - - -class Inventory(Schema): - elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) class Snapshot(EventWithOneDevice): @@ -130,57 +174,50 @@ class Snapshot(EventWithOneDevice): See docs for more info. """ - device = NestedOn(Device) # todo and when dumping? + uuid = UUID(required=True) + software = EnumField(SnapshotSoftware, + required=True, + description='The software that generated this Snapshot.') + version = Version(required=True, description='The version of the software.') + events = NestedOn(Event, many=True) # todo ensure only specific events are submitted + expected_events = EnumField(SnapshotExpectedEvents, + many=True, + data_key='expectedEvents', + description='Keep open this Snapshot until the following events' + 'are performed. Setting this value will activate' + 'the async Snapshot.') + device = NestedOn(Device) + elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) components = NestedOn(Component, many=True, description='A list of components that are inside of the device' 'at the moment of this Snapshot.' 'Order is preserved, so the component num 0 when' 'submitting is the component num 0 when returning it back.') - uuid = UUID(required=True) - version = Version(required=True, description='The version of the SnapshotSoftware.') - software = EnumField(SoftwareType, - required=True, - description='The software that generated this Snapshot.') - condition = Nested(Condition, required=True) - install = Nested(Installation) - elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) - inventory = Nested(Inventory) - color = Color(description='Main color of the device.') - orientation = EnumField(Orientation, description='Is the device main stand wider or larger?') - events = NestedOn(Event, many=True, dump_only=True) @validates_schema def validate_workbench_version(self, data: dict): - if data['software'] == SoftwareType.Workbench: + if data['software'] == SnapshotSoftware.Workbench: if data['version'] < app.config['MIN_WORKBENCH']: raise ValidationError( - 'Min. supported Workbench version is {}'.format(app.config['MIN_WORKBENCH']), + 'Min. supported Workbench algorithm_version is ' + '{}'.format(app.config['MIN_WORKBENCH']), field_names=['version'] ) @validates_schema def validate_components_only_workbench(self, data: dict): - if data['software'] != SoftwareType.Workbench: + if data['software'] != SnapshotSoftware.Workbench: if data['components'] is not None: raise ValidationError('Only Workbench can add component info', field_names=['components']) - @post_load - def normalize_nested(self, data: dict): - data.update(data.pop('condition')) - data['condition'] = data.pop('general', None) - data.update({'install_' + key: value for key, value in data.pop('install', {})}) - data['inventory_elapsed'] = data.get('inventory', {}).pop('elapsed', None) - return data - class Test(EventWithOneDevice): elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) - success = Boolean(required=True) -class TestHardDrive(Test): +class TestDataStorage(Test): length = EnumField(TestHardDriveLength, required=True) status = String(validate=Length(max=STR_SIZE), required=True) lifetime = TimeDelta(precision=TimeDelta.DAYS, required=True) diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index 24afd5de..93a59cab 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -4,9 +4,9 @@ from flask import request from sqlalchemy.util import OrderedSet from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Device -from ereuse_devicehub.resources.event.enums import SoftwareType -from ereuse_devicehub.resources.event.models import Event, Snapshot, TestHardDrive +from ereuse_devicehub.resources.device.models import Computer +from ereuse_devicehub.resources.enums import SnapshotSoftware +from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage from teal.resource import View @@ -31,12 +31,16 @@ class SnapshotView(View): # Note that if we set the device / components into the snapshot # model object, when we flush them to the db we will flush # snapshot, and we want to wait to flush snapshot at the end - device = s.pop('device') # type: Device - components = s.pop('components') if s['software'] == SoftwareType.Workbench else None + device = s.pop('device') # type: Computer + components = s.pop('components') if s['software'] == SnapshotSoftware.Workbench else None + if 'events' in s: + events = s.pop('events') + # todo perform events # noinspection PyArgumentList snapshot = Snapshot(**s) snapshot.device, snapshot.events = self.resource_def.sync.run(device, components) snapshot.components = snapshot.device.components + # todo compute rating # commit will change the order of the components by what # the DB wants. Let's get a copy of the list so we preserve order ordered_components = OrderedSet(x for x in snapshot.components) @@ -57,7 +61,7 @@ class TestHardDriveView(View): def post(self): t = request.get_json() # type: dict # noinspection PyArgumentList - test = TestHardDrive(snapshot_id=t.pop('snapshot'), device_id=t.pop('device'), **t) + test = TestDataStorage(snapshot_id=t.pop('snapshot'), device_id=t.pop('device'), **t) return test diff --git a/ereuse_devicehub/resources/image/__init__.py b/ereuse_devicehub/resources/image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/resources/image/models.py b/ereuse_devicehub/resources/image/models.py new file mode 100644 index 00000000..5c45f9ea --- /dev/null +++ b/ereuse_devicehub/resources/image/models.py @@ -0,0 +1,42 @@ +from uuid import uuid4 + +from sqlalchemy import BigInteger, Column, Enum as DBEnum, ForeignKey, Unicode +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import backref, relationship +from sqlalchemy.util import OrderedSet + +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.enums import ImageMimeTypes, Orientation +from ereuse_devicehub.resources.models import STR_BIG_SIZE, Thing +from teal.db import CASCADE_OWN + + +class ImageList(Thing): + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False) + device = relationship(Device, + primaryjoin=Device.id == device_id, + backref=backref('images', + lazy=True, + cascade=CASCADE_OWN, + order_by=lambda: ImageList.created, + collection_class=OrderedSet)) + + +class Image(Thing): + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + name = Column(Unicode(STR_BIG_SIZE), default='', nullable=False) + content = db.Column(db.LargeBinary, nullable=False) + file_format = db.Column(DBEnum(ImageMimeTypes), nullable=False) + orientation = db.Column(DBEnum(Orientation), nullable=False) + image_list_id = Column(UUID(as_uuid=True), ForeignKey(ImageList.id), nullable=False) + image_list = relationship(ImageList, + primaryjoin=ImageList.id == image_list_id, + backref=backref('images', + cascade=CASCADE_OWN, + order_by=lambda: Image.created, + collection_class=OrderedSet)) + + # todo make an image Field that converts to/from image object + # todo which metadata we get from Photobox? diff --git a/ereuse_devicehub/resources/image/models.pyi b/ereuse_devicehub/resources/image/models.pyi new file mode 100644 index 00000000..b49d03a0 --- /dev/null +++ b/ereuse_devicehub/resources/image/models.pyi @@ -0,0 +1,41 @@ +from uuid import UUID + +from sqlalchemy import Column +from sqlalchemy.orm import relationship + +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.enums import ImageMimeTypes, Orientation +from ereuse_devicehub.resources.models import Thing + + +class ImageList(Thing): + id = ... # type: Column + device = ... # type: Column + images = ... # type: relationship + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.id = ... # type: UUID + self.device = ... # type: Device + self.images = ... # types: List[Image] + + +class Image(Thing): + id = ... # type: Column + position = ... #type: Column + name = ... # type: Column + content = ... # type: Column + file_format = ... # type: Column + orientation = ... # type: Column + image_list = ... # type: relationship + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.id = ... # type: UUID + self.position = ... # type: int + self.name = '' # type: str + self.content = ... # type: bytes + self.file_format = ... # type: ImageMimeTypes + self.orientation = ... # type: Orientation + self.image_list_id = ... # type: UUID + self.image_list = ... # type: ImageList diff --git a/ereuse_devicehub/resources/models.py b/ereuse_devicehub/resources/models.py index acdd8bb9..190b374c 100644 --- a/ereuse_devicehub/resources/models.py +++ b/ereuse_devicehub/resources/models.py @@ -5,6 +5,7 @@ from ereuse_devicehub.db import db STR_SIZE = 64 STR_BIG_SIZE = 128 STR_SM_SIZE = 32 +STR_XSM_SIZE = 16 class Thing(db.Model): diff --git a/ereuse_devicehub/resources/models.pyi b/ereuse_devicehub/resources/models.pyi new file mode 100644 index 00000000..2c11ac87 --- /dev/null +++ b/ereuse_devicehub/resources/models.pyi @@ -0,0 +1,22 @@ +from datetime import datetime + +from sqlalchemy import Column + +from teal.db import Model + +STR_SIZE = 64 +STR_BIG_SIZE = 128 +STR_SM_SIZE = 32 +STR_XSM_SIZE = 16 + + +class Thing(Model): + t = ... # type: str + type = ... # type: str + updated = ... # type: Column + created = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.updated = ... # type: datetime + self.created = ... # type: datetime diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 8f9fdcd7..e0153506 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -17,7 +17,7 @@ class Tag(Thing): # If we link with the Organization object this instance # will be set as persistent and added to session # which is something we don't want to enforce by default - default=lambda: Organization.get_default_org().id) + default=lambda: Organization.get_default_org_id()) org = relationship(Organization, backref=backref('tags', lazy=True), primaryjoin=Organization.id == org_id, diff --git a/ereuse_devicehub/resources/user/models.py b/ereuse_devicehub/resources/user/models.py index 6200ddc7..1234d735 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -1,6 +1,6 @@ from uuid import uuid4 -from flask import current_app as app +from flask import current_app as app, g from sqlalchemy import Column, Unicode, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy_utils import CountryType, EmailType, PasswordType @@ -42,9 +42,12 @@ class Organization(Thing): ) @classmethod - def get_default_org(cls) -> 'Organization': + def get_default_org_id(cls) -> UUID: """Retrieves the default organization.""" - return Organization.query.filter_by(**app.config.get_namespace('ORGANIZATION_')).one() + return g.setdefault('org_id', + Organization.query.filter_by( + **app.config.get_namespace('ORGANIZATION_') + ).one().id) def __repr__(self) -> str: return ''.format(self) diff --git a/tests/files/1-device-with-components.snapshot.yaml b/tests/files/1-device-with-components.snapshot.yaml index e7249b5b..c40c0ef7 100644 --- a/tests/files/1-device-with-components.snapshot.yaml +++ b/tests/files/1-device-with-components.snapshot.yaml @@ -3,7 +3,6 @@ device: serialNumber: 'p1' model: 'p1' type: 'Desktop' -secured: False components: - manufacturer: 'p1c1m' serialNumber: 'p1c1s' @@ -18,9 +17,6 @@ components: serialNumber: 'p1c3s' type: 'GraphicCard' memory: 1.5 -condition: - appearance: 'A' - functionality: 'B' elapsed: 25 software: 'Workbench' uuid: '76860eca-c3fd-41f6-a801-6af7bd8cf832' diff --git a/tests/files/2-second-device-with-components-of-first.snapshot.yaml b/tests/files/2-second-device-with-components-of-first.snapshot.yaml index e0333ee8..26670711 100644 --- a/tests/files/2-second-device-with-components-of-first.snapshot.yaml +++ b/tests/files/2-second-device-with-components-of-first.snapshot.yaml @@ -3,7 +3,6 @@ device: serialNumber: 'p2s' model: 'p2' type: 'Microtower' -secured: False components: - manufacturer: 'p2c1m' serialNumber: 'p2c1s' @@ -14,9 +13,6 @@ components: speed: 1.23 cores: 2 type: 'Processor' -condition: - appearance: 'A' - functionality: 'B' elapsed: 25 software: 'Workbench' uuid: 'f2e02261-87a1-4a50-b9b7-92c0e476e5f2' diff --git a/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml b/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml index 968eda05..07508438 100644 --- a/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml +++ b/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml @@ -3,7 +3,6 @@ device: serialNumber: 'p1' model: 'p1' type: 'Desktop' -secured: False components: - manufacturer: 'p1c2m' serialNumber: 'p1c2s' @@ -15,9 +14,6 @@ components: serialNumber: 'p1c3s' type: 'GraphicCard' memory: 1.5 -condition: - appearance: 'C' - functionality: 'C' elapsed: 30 software: 'Workbench' uuid: '3be271b6-5ef4-47d8-8237-5e1133eebfc6' diff --git a/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml b/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml index 451ddd14..71fa3ca1 100644 --- a/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml +++ b/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml @@ -3,7 +3,6 @@ device: serialNumber: 'p1' model: 'p1' type: 'Desktop' -secured: False components: - manufacturer: 'p1c4m' serialNumber: 'p1c4s' @@ -13,9 +12,6 @@ components: serialNumber: 'p1c3s' type: 'GraphicCard' memory: 1.5 -condition: - appearance: 'A' - functionality: 'A' elapsed: 25 software: 'Workbench' uuid: 'fd007eb4-48e3-454a-8763-169491904c6e' diff --git a/tests/files/basic.snapshot.yaml b/tests/files/basic.snapshot.yaml index 9a9ccc95..afa9ea6b 100644 --- a/tests/files/basic.snapshot.yaml +++ b/tests/files/basic.snapshot.yaml @@ -1,12 +1,13 @@ -uuid: 'f5efd26e-8754-46bc-87bf-fbccc39d60d9' type: 'Snapshot' +uuid: 'f5efd26e-8754-46bc-87bf-fbccc39d60d9' version: '11.0' software: 'Workbench' -condition: - appearance: 'A' - functionality: 'B' - labelling: True - bios: 'B' +events: + - type: 'WorkbenchRate' + appearanceRange: 'A' + functionalityRange: 'B' + labelling: True + bios: 'B' elapsed: 4 device: type: 'Microtower' diff --git a/tests/files/erase-sectors.snapshot.yaml b/tests/files/erase-sectors.snapshot.yaml new file mode 100644 index 00000000..f84e50ba --- /dev/null +++ b/tests/files/erase-sectors.snapshot.yaml @@ -0,0 +1,42 @@ +uuid: '74caa7eb-2bad-4333-94f6-6f1b031d0775' +type: 'Snapshot' +version: '11.0' +software: 'Workbench' +elapsed: 4 +device: + type: 'Microtower' + serialNumber: 'pc1s' + model: 'pc1ml' + manufacturer: 'pc1mr' +components: + - type: 'SolidStateDrive' + serialNumber: 'c1s' + model: 'c1ml' + manufacturer: 'pc1mr' + erasure: + type: 'EraseSectors' + cleanWithZeros: True + startTime: '2018-06-01T08:12:06' + endTime: '2018-06-01T09:12:06' + secureRandomSteps: 20 + steps: + - type: 'StepZero' + error: False + startTime: '2018-06-01T08:15:00' + endTime: '2018-06-01T09:16:00' + secureRandomSteps: 1 + cleanWithZeros: True + - type: 'StepZero' + error: False + startTime: '2018-06-01T08:16:00' + endTime: '2018-06-01T09:17:00' + secureRandomSteps: 1 + cleanWithZeros: True + - type: 'GraphicCard' + serialNumber: 'gc1s' + model: 'gc1ml' + manufacturer: 'gc1mr' + - type: 'RamModule' + serialNumber: 'rm1s' + model: 'rm1ml' + manufacturer: 'rm1mr' diff --git a/tests/files/workbench-server-1.snapshot.yaml b/tests/files/workbench-server-1.snapshot.yaml new file mode 100644 index 00000000..0966d1a4 --- /dev/null +++ b/tests/files/workbench-server-1.snapshot.yaml @@ -0,0 +1,87 @@ +# A Snapshot Phase 1 with a device +# and 1 GraphicCard, 2 RamModule, 1 Processor, 1 SSD, 1 HDD, 1 Motherboard +# Prerequisites: +# - 2 tags: 'tag1' and 'tag2' from the default org +# All numbers are invented + + +type: 'Snapshot' +uuid: 'cb8ce6b5-6a1b-4084-b5b9-d8fadad2a015' +version: '11.0' +software: 'Workbench' +startTime: '2018-06-08T17:52:00' +expectedEvents: ['TestDataStorage', 'StressTest', 'EraseSectors', 'Install'] +device: + type: 'Microtower' + serialNumber: 'd1s' + model: 'd1ml' + manufacturer: 'd1mr' + tags: + - type: 'Tag' + id: 'tag1' + - type: 'Tag' + id: 'tag2' +events: + - type: 'WorkbenchRate' + appearanceRange: 'A' + functionalityRange: 'B' + - type: 'BenchmarkRamSysbench' + rate: 2444 +components: + - type: 'GraphicCard' + serialNumber: 'gc1-1s' + model: 'gc1-1ml' + manufacturer: 'gc1-1mr' + - type: 'RamModule' + serialNumber: 'rm1-1s' + model: 'rm1-1ml' + manufacturer: 'rm1-1mr' + - type: 'RamModule' + serialNumber: 'rm2-1s' + model: 'rm2-1ml' + manufacturer: 'rm2-1mr' + - type: 'Processor' + model: 'p1-1s' + manufacturer: 'p1-1mr' + benchmarks: + - type: 'BenchmarkProcessor' + rate: 2410 + - type: 'BenchmarkProcessorSysbench' + rate: 4400 + - type: 'SolidStateDrive' + serialNumber: 'ssd1-1s' + model: 'ssd1-1ml' + manufacturer: 'ssd1-1mr' + benchmark: + type: 'BenchmarkDataStorage' + readingSpeed: 20 + writingSpeed: 15 + test: + type: 'TestDataStorage' + firstError: 0 + error: False + status: 'Completed without error' + length: 'Short' + lifetime: 99 + passedLifeTime: 99 + assessment: True + powerCycleCount: 11 + reallocatedSectorCount: 2 + powerCycleCount: 4 + reportedUncorrectableErrors: 1 + commandTimeout: 11 + currentPendingSectorCount: 1 + offlineUncorrectable: 33 + remainingLifetimePercentage: 1 + - type: 'HardDrive' + serialNumber: 'hdd1-1s' + model: 'hdd1-1ml' + manufacturer: 'hdd1-1mr' + benchmark: + type: 'BenchmarkDataStorage' + readingSpeed: 10 + writingSpeed: 5 + - type: 'Motherboard' + serialNumber: 'mb1-1s' + model: 'mb1-1ml' + manufacturer: 'mb1-1mr' diff --git a/tests/files/workbench-server-2.stress-test.yaml b/tests/files/workbench-server-2.stress-test.yaml new file mode 100644 index 00000000..c762e16d --- /dev/null +++ b/tests/files/workbench-server-2.stress-test.yaml @@ -0,0 +1,12 @@ +# A Snapshot Phase 1 with a device +# and 1 GraphicCard, 2 RamModule, 1 Processor, 1 SSD, 1 HDD, 1 Motherboard +# Prerequisites: +# - workbench-server-1.snapshot.yaml +# Requisites: +# - Set the ``snapshot`` field to ``workbench-server-1...`` +# All numbers are invented + +type: 'StressTest' +elapsed: 300 +error: False +snapshot: None # fulfill! \ No newline at end of file diff --git a/tests/files/workbench-server-3.erase.yaml b/tests/files/workbench-server-3.erase.yaml new file mode 100644 index 00000000..a19935ce --- /dev/null +++ b/tests/files/workbench-server-3.erase.yaml @@ -0,0 +1,21 @@ +# A Snapshot Phase 1 with a device +# and 1 GraphicCard, 2 RamModule, 1 Processor, 1 SSD, 1 HDD, 1 Motherboard +# Prerequisites: +# - workbench-server-1.snapshot.yaml +# Requisites: +# - Set the ``snapshot`` field to ``workbench-server-1...`` +# All numbers are invented + +type: 'EraseSectors' +error: False +snapshot: None # fulfill! +device: None # fulfill! +cleanWithZeros: False +startTime: 2018-01-01T10:10:10 +endTime: 2018-01-01T12:10:10 +secureRandomSteps: 0 +steps: + - type: 'StepRandom' + startTime: '2018-01-01T10:10:10' + endTime: '2018-01-01T12:10:10' + error: False diff --git a/tests/files/workbench-server-4.install.yaml b/tests/files/workbench-server-4.install.yaml new file mode 100644 index 00000000..33091dc2 --- /dev/null +++ b/tests/files/workbench-server-4.install.yaml @@ -0,0 +1,14 @@ +# A Snapshot Phase 1 with a device +# and 1 GraphicCard, 2 RamModule, 1 Processor, 1 SSD, 1 HDD, 1 Motherboard +# Prerequisites: +# - workbench-server-1.snapshot.yaml +# Requisites: +# - Set the ``snapshot`` field to ``workbench-server-1...`` +# All numbers are invented + +type: 'Install' +elapsed: 420 +error: False +snapshot: None # fulfill! +device: None # fulfill! +name: 'LinuxMint 18.01 32b' \ No newline at end of file diff --git a/tests/test_device.py b/tests/test_device.py index 87bfcb22..0eb2f8f7 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -2,6 +2,8 @@ from datetime import timedelta from uuid import UUID import pytest +from colour import Color + from ereuse_utils.naming import Naming from pytest import raises from sqlalchemy.util import OrderedSet @@ -79,15 +81,13 @@ def test_physical_properties(): model='ml', manufacturer='mr', width=2.0, - pid='abc') + color=Color()) pc = Computer() pc.components.add(c) db.session.add(pc) db.session.commit() assert c.physical_properties == { - 'gid': None, 'usb': 3, - 'pid': 'abc', 'serial_number': 'sn', 'pcmcia': None, 'model': 'ml', @@ -97,7 +97,9 @@ def test_physical_properties(): 'manufacturer': 'mr', 'weight': None, 'height': None, - 'width': 2.0 + 'width': 2.0, + 'color': Color(), + 'depth': None } @@ -358,16 +360,15 @@ def test_get_device(app: Devicehub, user: UserClient): db.session.add(pc) db.session.add(Test(device=pc, elapsed=timedelta(seconds=4), - success=True, + error=False, author=User(email='bar@bar.com'))) db.session.commit() pc, _ = user.get(res=Device, item=1) assert len(pc['events']) == 1 assert pc['events'][0]['type'] == 'Test' - assert pc['events'][0]['id'] == 1 assert pc['events'][0]['device'] == 1 assert pc['events'][0]['elapsed'] == 4 - assert pc['events'][0]['success'] == True + assert not pc['events'][0]['error'] assert UUID(pc['events'][0]['author']) assert 'events_components' not in pc, 'events_components are internal use only' assert 'events_one' not in pc, 'they are internal use only' diff --git a/tests/test_event.py b/tests/test_event.py index 29f53cc2..0996466d 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,9 +1,14 @@ +from datetime import datetime, timedelta + import pytest from flask import g from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Device -from ereuse_devicehub.resources.event.models import EventWithOneDevice +from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, \ + SolidStateDrive +from ereuse_devicehub.resources.enums import TestHardDriveLength +from ereuse_devicehub.resources.event.models import EraseBasic, EraseSectors, \ + EventWithOneDevice, Install, StepZero, TestDataStorage from tests.conftest import create_user @@ -22,3 +27,101 @@ def test_author(): assert e.author_id is None db.session.commit() assert e.author == user + + +@pytest.mark.usefixtures('auth_app_context') +def test_erase_basic(): + erasure = EraseBasic( + device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), + clean_with_zeros=True, + start_time=datetime.now(), + end_time=datetime.now(), + secure_random_steps=25, + error=False + ) + db.session.add(erasure) + db.session.commit() + db_erasure = EraseBasic.query.one() + assert erasure == db_erasure + assert next(iter(db_erasure.device.events)) == erasure + + +@pytest.mark.usefixtures('auth_app_context') +def test_validate_device_data_storage(): + """Checks the validation for data-storage-only events works.""" + # We can't set a GraphicCard + with pytest.raises(TypeError, + message='EraseBasic.device must be a DataStorage ' + 'but you passed '): + EraseBasic( + device=GraphicCard(serial_number='foo', manufacturer='bar', model='foo-bar'), + clean_with_zeros=True, + start_time=datetime.now(), + end_time=datetime.now(), + secure_random_steps=25, + error=False + ) + + +@pytest.mark.usefixtures('auth_app_context') +def test_erase_sectors_steps(): + erasure = EraseSectors( + device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), + clean_with_zeros=True, + start_time=datetime.now(), + end_time=datetime.now(), + secure_random_steps=25, + error=False, + steps=[ + StepZero(error=False, + start_time=datetime.now(), + end_time=datetime.now(), + secure_random_steps=1, + clean_with_zeros=True), + StepZero(error=False, + start_time=datetime.now(), + end_time=datetime.now(), + secure_random_steps=2, + clean_with_zeros=True), + StepZero(error=False, + start_time=datetime.now(), + end_time=datetime.now(), + secure_random_steps=3, + clean_with_zeros=True) + ] + ) + db.session.add(erasure) + db.session.commit() + db_erasure = EraseSectors.query.one() + # Steps are in order + assert db_erasure.steps[0].secure_random_steps == 1 + assert db_erasure.steps[0].num == 0 + assert db_erasure.steps[1].secure_random_steps == 2 + assert db_erasure.steps[1].num == 1 + assert db_erasure.steps[2].secure_random_steps == 3 + assert db_erasure.steps[2].num == 2 + + +@pytest.mark.usefixtures('auth_app_context') +def test_test_data_storage(): + test = TestDataStorage( + device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'), + error=False, + elapsed=timedelta(minutes=25), + length=TestHardDriveLength.Short, + status='OK!', + lifetime=timedelta(days=120) + ) + db.session.add(test) + db.session.commit() + assert TestDataStorage.query.one() + + +@pytest.mark.usefixtures('auth_app_context') +def test_install(): + hdd = HardDrive(serial_number='sn') + install = Install(name='LinuxMint 18.04 es', + elapsed=timedelta(seconds=25), + device=hdd) + db.session.add(install) + db.session.commit() diff --git a/tests/test_organization.py b/tests/test_organization.py index c5506ff6..074f9714 100644 --- a/tests/test_organization.py +++ b/tests/test_organization.py @@ -1,3 +1,5 @@ +from uuid import UUID + import pytest from ereuse_devicehub.config import DevicehubConfig @@ -13,4 +15,4 @@ def test_default_org_exists(config: DevicehubConfig): """ assert Organization.query.filter_by(name=config.ORGANIZATION_NAME, tax_id=config.ORGANIZATION_TAX_ID).one() - assert Organization.get_default_org().name == config.ORGANIZATION_NAME + assert isinstance(Organization.get_default_org_id(), UUID) diff --git a/tests/test_rate.py b/tests/test_rate.py new file mode 100644 index 00000000..54b94609 --- /dev/null +++ b/tests/test_rate.py @@ -0,0 +1,40 @@ +from distutils.version import StrictVersion + +import pytest + +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.device.models import Microtower +from ereuse_devicehub.resources.enums import Bios, ImageMimeTypes, Orientation, RatingSoftware +from ereuse_devicehub.resources.event.models import PhotoboxRate, WorkbenchRate +from ereuse_devicehub.resources.image.models import Image, ImageList + + +@pytest.mark.usefixtures('auth_app_context') +def test_workbench_rate(): + rate = WorkbenchRate(processor=0.1, + ram=1.0, + bios=Bios.A, + labelling=False, + graphic_card=0.1, + data_storage=4.1, + algorithm_software=RatingSoftware.Ereuse, + algorithm_version=StrictVersion('1.0'), + device=Microtower(serial_number='24')) + db.session.add(rate) + db.session.commit() + + +@pytest.mark.usefixtures('auth_app_context') +def test_photobox_rate(): + pc = Microtower(serial_number='24') + image = Image(name='foo', + content=b'123', + file_format=ImageMimeTypes.jpg, + orientation=Orientation.Horizontal, + image_list=ImageList(device=pc)) + rate = PhotoboxRate(image=image, + algorithm_software=RatingSoftware.Ereuse, + algorithm_version=StrictVersion('1.0'), + device=pc) + db.session.add(rate) + db.session.commit() diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index c953d371..bb95c712 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta -from typing import List +from distutils.version import StrictVersion +from typing import List, Tuple from uuid import uuid4 import pytest @@ -8,10 +9,11 @@ from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.device.exceptions import NeedsId -from ereuse_devicehub.resources.device.models import Device, Microtower +from ereuse_devicehub.resources.device.models import Device, Microtower, SolidStateDrive from ereuse_devicehub.resources.device.sync import MismatchBetweenTagsAndHid -from ereuse_devicehub.resources.event.models import Appearance, Bios, Event, Functionality, \ - Snapshot, SnapshotRequest, SoftwareType +from ereuse_devicehub.resources.enums import Bios, RatingSoftware, SnapshotSoftware +from ereuse_devicehub.resources.event.models import EraseBasic, Event, Snapshot, SnapshotRequest, \ + WorkbenchRate from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.user.models import User from tests.conftest import file @@ -19,7 +21,8 @@ from tests.conftest import file def assert_similar_device(device1: dict, device2: dict): """ - Like Model.is_similar() but adapted for testing. + Like :class:`ereuse_devicehub.resources.device.models.Device. + is_similar()` but adapted for testing. """ assert isinstance(device1, dict) and device1 assert isinstance(device2, dict) and device2 @@ -39,10 +42,21 @@ def assert_similar_components(components1: List[dict], components2: List[dict]): def snapshot_and_check(user: UserClient, input_snapshot: dict, - event_types: tuple or list = tuple(), + event_types: Tuple[str] = tuple(), perform_second_snapshot=True) -> dict: """ - P + Performs a Snapshot and then checks if the result is ok: + + - There have been performed the types of events and in the same + order as described in the passed-in ``event_types``. + - The inputted devices are similar to the resulted ones. + - There is no Remove event after the first Add. + - All input components are now inside the parent device. + + Optionally, it can perform a second Snapshot which should + perform an exact result, except for the events. + + :return: The last resulting snapshot. """ snapshot, _ = user.post(res=Snapshot, data=input_snapshot) assert tuple(e['type'] for e in snapshot['events']) == event_types @@ -76,22 +90,25 @@ def test_snapshot_model(): snapshot = Snapshot(uuid=uuid4(), date=datetime.now(), version='1.0', - software=SoftwareType.DesktopApp, - appearance=Appearance.A, - appearance_score=5, - functionality=Functionality.A, - functionality_score=5, - labelling=False, - bios=Bios.C, - condition=5, + software=SnapshotSoftware.DesktopApp, elapsed=timedelta(seconds=25)) snapshot.device = device snapshot.request = SnapshotRequest(request={'foo': 'bar'}) - + snapshot.events.add(WorkbenchRate(processor=0.1, + ram=1.0, + bios=Bios.A, + labelling=False, + graphic_card=0.1, + data_storage=4.1, + algorithm_software=RatingSoftware.Ereuse, + algorithm_version=StrictVersion('1.0'), + device=device)) db.session.add(snapshot) db.session.commit() device = Microtower.query.one() # type: Microtower - assert device.events_one[0].type == Snapshot.__name__ + e1, e2 = device.events + assert isinstance(e1, Snapshot), 'Creation order must be preserved: 1. snapshot, 2. WR' + assert isinstance(e2, WorkbenchRate) db.session.delete(device) db.session.commit() assert Snapshot.query.one_or_none() is None @@ -137,7 +154,7 @@ def test_snapshot_component_add_remove(user: UserClient): [c['serialNumber'] for c in e['components']], e.get('snapshot', {}).get('id', None) ) - for e in (user.get(res=Event, item=e['id'])[0] for e in events) + for e in user.get_many(res=Event, resources=events, key='id') ) # We add the first device (2 times). The distribution of components @@ -255,7 +272,7 @@ def _test_snapshot_computer_no_hid(user: UserClient): def test_snapshot_mismatch_id(): """Tests uploading a device with an ID from another device.""" - # Note that this won't happen as in this new version + # Note that this won't happen as in this new algorithm_version # the ID is not used in the Snapshot process pass @@ -279,3 +296,30 @@ def test_snapshot_tag_inner_tag_mismatch_between_tags_and_hid(user: UserClient, user.post(pc2, res=Snapshot) # PC2 uploads well pc2['device']['tags'] = [{'type': 'Tag', 'id': tag_id}] # Set tag from pc1 to pc2 user.post(pc2, res=Snapshot, status=MismatchBetweenTagsAndHid) + + +def test_erase(user: UserClient): + """Tests a Snapshot with EraseSectors.""" + s = file('erase-sectors.snapshot') + snapshot = snapshot_and_check(user, s, ('EraseSectors',), perform_second_snapshot=True) + storage, *_ = snapshot['components'] + assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order' + storage, _ = user.get(res=SolidStateDrive, item=storage['id']) # Let's get storage events too + _snapshot1, _snapshot2, erasure = storage['events'] + assert erasure['type'] == 'EraseSectors' + assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot' + assert snapshot == _snapshot2 + erasure, _ = user.get(res=EraseBasic, item=erasure['id']) + assert len(erasure['steps']) == 2 + assert erasure['steps'][0]['startingTime'] == '2018-06-01T08:15:00' + assert erasure['steps'][0]['endingTime'] == '2018-06-01T09:16:00' + assert erasure['steps'][1]['endingTime'] == '2018-06-01T08:16:00' + assert erasure['steps'][1]['endingTime'] == '2018-06-01T09:17:00' + assert erasure['device']['id'] == storage['id'] + for step in erasure['steps']: + assert step['type'] == 'StepZero' + assert step['error'] is False + assert step['secureRandomSteps'] == 1 + assert step['cleanWithZeros'] is True + assert 'num' not in step + assert step['erasure'] == erasure['id'] diff --git a/tests/test_workbench.py b/tests/test_workbench.py new file mode 100644 index 00000000..058784ec --- /dev/null +++ b/tests/test_workbench.py @@ -0,0 +1,115 @@ +""" +Tests that emulates the behaviour of a WorkbenchServer. +""" +from ereuse_devicehub.client import UserClient +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.event.models import EraseSectors, Install, Snapshot, \ + StressTest +from tests.conftest import file + + +def test_workbench_server_phases(user: UserClient): + """ + Tests the phases described in the docs section `Snapshots from + Workbench `_. + """ + # 1. Snapshot with sync / rate / benchmarks / test data storage + s = file('workbench-server-1.snapshot') + snapshot, _ = user.post(res=Snapshot, data=s) + assert not snapshot['closed'], 'Snapshot must be waiting for the new events' + + # 2. stress test + st = file('workbench-server-2.stress-test') + st['snapshot'] = snapshot['id'] + stress_test, _ = user.post(res=StressTest, data=st) + + # 3. erase + ssd_id, hdd_id = snapshot['components'][4]['id'], snapshot['components'][5]['id'] + e = file('workbench-server-3.erase') + e['snapshot'], e['device'] = snapshot['id'], ssd_id + erase1, _ = user.post(res=EraseSectors, data=e) + + # 3 bis. a second erase + e = file('workbench-server-3.erase') + e['snapshot'], e['device'] = snapshot['id'], hdd_id + erase2, _ = user.post(res=EraseSectors, data=e) + + # 4. Install + i = file('workbench-server-4.install') + i['snapshot'], i['device'] = snapshot['id'], ssd_id + install, _ = user.post(res=Install, data=i) + + # Check events have been appended in Snapshot and devices + # and that Snapshot is closed + snapshot, _ = user.get(res=Snapshot, item=snapshot['id']) + events = snapshot['events'] + assert len(events) == 9 + assert events[0]['type'] == 'Rate' + assert events[0]['device'] == 1 + assert events[0]['closed'] + assert events[0]['type'] == 'WorkbenchRate' + assert events[0]['device'] == 1 + assert events[1]['type'] == 'BenchmarkProcessor' + assert events[1]['device'] == 5 + assert events[2]['type'] == 'BenchmarkProcessorSysbench' + assert events[2]['device'] == 5 + assert events[3]['type'] == 'BenchmarkDataStorage' + assert events[3]['device'] == 6 + assert events[4]['type'] == 'TestDataStorage' + assert events[4]['device'] == 6 + assert events[4]['type'] == 'BenchmarkDataStorage' + assert events[4]['device'] == 7 + assert events[5]['type'] == 'StressTest' + assert events[5]['device'] == 1 + assert events[6]['type'] == 'EraseSectors' + assert events[6]['device'] == 6 + assert events[7]['type'] == 'EraseSectors' + assert events[7]['device'] == 7 + assert events[8]['type'] == 'Install' + assert events[8]['device'] == 6 + assert snapshot['closed'] + assert not snapshot['error'] + + pc, _ = user.get(res=Device, item=snapshot['id']) + assert len(pc['events']) == 10 # todo shall I add child events? + + +def test_workbench_server_condensed(user: UserClient): + """ + As :def:`.test_workbench_server_phases` but all the events + condensed in only one big ``Snapshot`` file, as described + in the docs. + """ + s = file('workbench-server-1.snapshot') + s['events'].append(file('workbench-server-2.stress-test')) + s['components'][5]['erasure'] = file('workbench-server-3.erase') + s['components'][5]['installation'] = file('workbench-server-4.install') + s['components'][6]['erasure'] = file('workbench-server-3.erase') + snapshot, _ = user.post(res=Snapshot, data=s) + events = snapshot['events'] + assert events[0]['type'] == 'Rate' + assert events[0]['device'] == 1 + assert events[0]['closed'] + assert events[0]['type'] == 'WorkbenchRate' + assert events[0]['device'] == 1 + assert events[1]['type'] == 'BenchmarkProcessor' + assert events[1]['device'] == 5 + assert events[2]['type'] == 'BenchmarkProcessorSysbench' + assert events[2]['device'] == 5 + assert events[3]['type'] == 'BenchmarkDataStorage' + assert events[3]['device'] == 6 + assert events[4]['type'] == 'TestDataStorage' + assert events[4]['device'] == 6 + assert events[4]['type'] == 'BenchmarkDataStorage' + assert events[4]['device'] == 7 + assert events[5]['type'] == 'StressTest' + assert events[5]['device'] == 1 + assert events[6]['type'] == 'EraseSectors' + assert events[6]['device'] == 6 + assert events[7]['type'] == 'EraseSectors' + assert events[7]['device'] == 7 + assert events[8]['type'] == 'Install' + assert events[8]['device'] == 6 + assert snapshot['closed'] + assert not snapshot['error']