diff --git a/.gitignore b/.gitignore index c576d9e..c1846ca 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ resources/ticket.css resources/ticket.html db.* zeosocket -ghostdriver.log \ No newline at end of file +ghostdriver.log +celerybeat-schedule.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1be1e0c..14fe2ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,6 @@ RUN pip install selenium==2.44.0 RUN pip install pifacecommon==4.1.2 RUN pip install pifacedigitalio==3.0.5 RUN pip install jinja2==2.7.3 -RUN pip install hashids==1.0.3 RUN pip install persistent==4.0.8 RUN pip install ZODB==4.1.0 RUN pip install ZODB3==3.11.0 @@ -70,20 +69,19 @@ RUN cd python-epson-printer-1.6 && python setup.py install ADD figureraspbian /figure/figureraspbian WORKDIR /figure RUN mkdir -p media/images media/snapshots media/tickets resources -RUN mkdir -p /var/log /var/run /var/db /var/images /var/snapshots /var/tickets /var/rabbitmq +RUN mkdir -p /var/log /var/run /var/rabbitmq +ADD ./install-piface-real-time-clock.sh /install-piface-real-time-clock.sh ENV LANG C.UTF-8 ENV C_FORCE_ROOT true -ENV DB_PATH /var/db/db.fs +ENV DB_PATH /var/data/db.fs ENV FIGURE_DIR /figure/figureraspbian -ENV IMAGE_DIR /var/images +ENV IMAGE_DIR /data/images ENV PHANTOMJS_PATH /phantomjs-linux-armv6l-master/phantomjs-1.9.0-linux-armv6l/bin/phantomjs -ENV RESOURCE_DIR /figure/resources -ENV SNAPSHOT_DIR /var/snapshots -ENV TICKET_CSS_PATH /var/ticket.css -ENV TICKET_HTML_PATH /var/ticket.html -ENV TICKET_DIR /var/tickets +ENV RESOURCE_DIR /data/resources +ENV SNAPSHOT_DIR /data/snapshots +ENV TICKET_DIR /data/tickets ENV ZEO_SOCKET /var/run/zeo.sock RUN pip install supervisor diff --git a/figureraspbian/__main__.py b/figureraspbian/__main__.py index 230dab9..f4a8203 100644 --- a/figureraspbian/__main__.py +++ b/figureraspbian/__main__.py @@ -36,16 +36,10 @@ def get_listener(): logger.info("Initializing Figure application...") - # Make sure database is correctly initialized - with managed(Database()) as db: - if utils.internet_on(): - logging.info("Got an internet connection. Initializing database...") - db.update() - elif db.is_initialized(): - logging.info("No internet connection but database was already initialized during a previous runtime") - pass - else: - logging.warning("Database could not be initialized.") + logger.info("Initializing database...") + database = Database() + database.open() + database.close() listener = get_listener() diff --git a/figureraspbian/api.py b/figureraspbian/api.py index fb8cb28..0fc7326 100644 --- a/figureraspbian/api.py +++ b/figureraspbian/api.py @@ -32,14 +32,14 @@ def get_installation(): raise ApiException("Failed retrieving installation") -def get_scenario(scenario_id): - url = "%s/scenarios/%s/?fields=name,ticket_template" % (settings.API_HOST, scenario_id) - r = session.get(url=url, timeout=10) +def get_codes(installation): + url = "%s/installations/%s/codes/" % (settings.API_HOST, installation) + r = session.get(url=url, timeout=20) if r.status_code == 200: r.encoding = 'utf-8' - return json.loads(r.text) + return json.loads(r.text)['codes'] else: - raise ApiException("Failed retrieving scenario") + raise ApiException('Fail retrieving codes') def download(url, path): @@ -48,17 +48,14 @@ def download(url, path): """ local_name = url2name(url) req = urllib2.Request(url) - try: - r = urllib2.urlopen(req, timeout=10) - if r.url != url: - # if we were redirected, the real file name we take from the final URL - local_name = url2name(r.url) - path_to_file = join(path, local_name) - with open(path_to_file, 'wb+') as f: - f.write(r.read()) - return path_to_file - except urllib2.HTTPError as e: - raise ApiException('Failed downloading resource %s with error %s' % (url, e.msg)) + r = urllib2.urlopen(req, timeout=10) + if r.url != url: + # if we were redirected, the real file name we take from the final URL + local_name = url2name(r.url) + path_to_file = join(path, local_name) + with open(path_to_file, 'wb+') as f: + f.write(r.read()) + return path_to_file def create_ticket(ticket): diff --git a/figureraspbian/db.py b/figureraspbian/db.py index ef0c88c..3e9b8b8 100644 --- a/figureraspbian/db.py +++ b/figureraspbian/db.py @@ -4,12 +4,17 @@ import logging logging.basicConfig(level='INFO') logger = logging.getLogger(__name__) -import traceback +import time from ZEO import ClientStorage from ZODB import DB +from ZODB.POSException import ConflictError +from BTrees.OOBTree import OOBTree import transaction import persistent +from requests.exceptions import Timeout, ConnectionError +import urllib2 + from . import settings, api @@ -26,74 +31,144 @@ def managed(database): database.close() -class Database(persistent.Persistent): +class Database(object): def __init__(self): storage = ClientStorage.ClientStorage(settings.ZEO_SOCKET) self.db = DB(storage) - self.data = None + self.dbroot = None def open(self): - self.data = self.db.open().root() + self.dbroot = self.db.open().root() + if 'installation' not in self.dbroot: + installation = Installation() + self.dbroot['installation'] = installation + transaction.commit() + installation.update() + if 'tickets' not in self.dbroot: + self.dbroot['tickets'] = TicketsGallery() + transaction.commit() + + def close(self): + self.db.close() - def clear(self): - self.data.clear() - transaction.commit() + +class Installation(persistent.Persistent): + + def __init__(self): + self.id = None + self.codes = None + self.start = None + self.end = None + self.scenario = None + self.ticket_template = None def update(self): + """ Update the installation from Figure API """ try: installation = api.get_installation() if installation is not None: - scenario = api.get_scenario(installation['scenario_obj']['id']) - ticket_template = scenario['ticket_template'] - for image in ticket_template['images_objects']: + is_new = self.id != installation['id'] + self.start = installation['start'] + self.end = installation['end'] + self.id = installation['id'] + self.scenario = installation['scenario'] + self.ticket_template = self.scenario['ticket_template'] + for image in self.ticket_template['images']: api.download(image['media'], settings.IMAGE_DIR) - for image_variable in ticket_template['image_variables_objects']: + for image_variable in self.ticket_template['image_variables']: for image in image_variable['items']: api.download(image['media'], settings.IMAGE_DIR) ticket_css_url = "%s/%s" % (settings.API_HOST, 'static/css/ticket.css') + if is_new: + self.codes = api.get_codes(self.id) api.download(ticket_css_url, settings.RESOURCE_DIR) - self.data['installation'] = installation - self.data['scenario'] = scenario + else: + self.id = None + self.codes = None + self.start = None + self.end = None + self.scenario = None + self.ticket_template = None + transaction.commit() + except (api.ApiException, Timeout, ConnectionError, ConflictError, urllib2.HTTPError) as e: + logger.exception(e) + transaction.abort() + + def get_code(self): + # claim a code + while True: + try: + code = self.codes.pop() + self._p_changed = 1 transaction.commit() - except Exception: - logger.error(traceback.format_exc()) + except ConflictError: + # Conflict occurred; this process should abort, + # wait for a little bit, then try again. + transaction.abort() + time.sleep(1) + else: + # No ConflictError exception raised, so break + # out of the enclosing while loop. + return code - def is_initialized(self): - return 'installation' in self.data - def check_initialized(func): - def check(self): - if self.is_initialized() is False: - raise NotInitializedError("Db was not yet initialized") - return func(self) - return check +class TicketsGallery(persistent.Persistent): - @check_initialized - def installation(self): - return self.data['installation'] - - @check_initialized - def scenario(self): - return self.data['scenario'] - - @check_initialized - def ticket_template(self): - return self.scenario()['ticket_template'] - - @check_initialized - def text_variables(self): - return self.ticket_template()['text_variables_objects'] + def __init__(self): + self._tickets = OOBTree() + + def add_ticket(self, ticket): + """ + Add a ticket to the gallery. + """ + while 1: + try: + self._tickets[ticket['dt']] = ticket + self._tickets[ticket['dt']]['uploaded'] = False + transaction.commit() + except ConflictError: + # Conflict occurred; this process should abort, + # wait for a little bit, then try again. + transaction.abort() + time.sleep(1) + else: + # No ConflictError exception raised, so break + # out of the enclosing while loop. + break + + def upload_tickets(self): + """ + Upload tickets + """ + + for _, ticket in self._tickets.items(): + if not ticket['uploaded']: + try: + # upload ticket + api.create_ticket(ticket) + while True: + try: + ticket['uploaded'] = True + transaction.commit() + except ConflictError: + # Conflict occurred; this process should abort, + # wait for a little bit, then try again. + transaction.abort() + time.sleep(1) + else: + # No ConflictError exception raised, so break + # out of the enclosing while loop. + break + except api.ApiException as e: + # Api error, proceed with remaining tickets + logger.exception(e) + except (Timeout, ConnectionError) as e: + # We might have loose internet connection, break for loop + logger.exception(e) + break - @check_initialized - def image_variables(self): - return self.ticket_template()['image_variables_objects'] - @check_initialized - def images(self): - return self.ticket_template()['images_objects'] - def close(self): - self.db.close() diff --git a/figureraspbian/devices/camera.py b/figureraspbian/devices/camera.py index 0e3cbbd..8e4b1c1 100644 --- a/figureraspbian/devices/camera.py +++ b/figureraspbian/devices/camera.py @@ -81,7 +81,7 @@ def capture(self, installation): im = im.crop((left, top, right, bottom)) else: raise Exception("Unknown camera type") - im = im.resize((512, 512), Image.ANTIALIAS) + im = im.resize((1024, 1024), Image.ANTIALIAS) im.save(path) return path diff --git a/figureraspbian/devices/light.py b/figureraspbian/devices/light.py index e8f8854..97e2856 100644 --- a/figureraspbian/devices/light.py +++ b/figureraspbian/devices/light.py @@ -18,7 +18,8 @@ def flash_off(self): class LEDPanelLight(Light): - pifacedigital = pifacedigitalio.PiFaceDigital() + def __init__(self): + self.pifacedigital = pifacedigitalio.PiFaceDigital() def flash_on(self): self.pifacedigital.output_pins[0].turn_on() diff --git a/figureraspbian/processus.py b/figureraspbian/processus.py index ca33f22..6b3e9aa 100644 --- a/figureraspbian/processus.py +++ b/figureraspbian/processus.py @@ -8,7 +8,6 @@ import logging logging.basicConfig(level='INFO') logger = logging.getLogger(__name__) -import traceback import codecs from selenium import webdriver @@ -27,75 +26,78 @@ def run(): with managed(Database()) as db: try: - # check if installation is not finished - end = datetime.strptime(db.installation()['end'], '%Y-%m-%dT%H:%M:%SZ') - end = end.replace(tzinfo=pytz.UTC) - - if end > datetime.now(pytz.timezone(settings.TIMEZONE)): - - # Retrieve necessary information from database - installation = db.installation()['id'] - ticket_template = db.ticket_template() - - # Initialize blinking task - blinking_task = None - # Set Output to False - devices.OUTPUT.set(True) - - # Take a snapshot - start = time.time() - snapshot = devices.CAMERA.capture(installation) - end = time.time() - logger.info('Snapshot capture successfully executed in %s seconds', end - start) - # Start blinking - blinking_task = devices.OUTPUT.blink() - - # Render ticket - start = time.time() - renderer = TicketRenderer(ticket_template['html'], - ticket_template['text_variables_objects'], - ticket_template['image_variables_objects'], - ticket_template['images_objects']) - html, dt, code, random_text_selections, random_image_selections = \ - renderer.render(installation, snapshot) - - with codecs.open(settings.TICKET_HTML_PATH, 'wb+', 'utf-8') as ticket: - ticket.write(html) - url = "file://%s" % settings.TICKET_HTML_PATH - phantom_js.get(url) - ticket = join(settings.TICKET_DIR, basename(snapshot)) - phantom_js.save_screenshot(ticket) - end = time.time() - logger.info('Ticket successfully rendered in %s seconds', end - start) - - # Print ticket - start = time.time() - devices.PRINTER.print_ticket(ticket) - end = time.time() - logger.info('Ticket successfully printed in %s seconds', end - start) - - # Stop blinking - blinking_task.terminate() - - # Set Output to True - devices.OUTPUT.set(False) - - # add task upload ticket task to the queue - ticket = { - 'installation': installation, - 'snapshot': snapshot, - 'ticket': ticket, - 'dt': dt, - 'code': code, - 'random_text_selections': random_text_selections, - 'random_image_selections': random_image_selections - } - tasks.create_ticket.delay(ticket) - else: - logger.warning("Current installation has ended. Skipping processus execution") + installation = db.dbroot['installation'] + + if installation is not None: + # Database is initialized ! + + # check if installation is not finished + end = datetime.strptime(installation.end, '%Y-%m-%dT%H:%M:%SZ') + end = end.replace(tzinfo=pytz.UTC) + + if end > datetime.now(pytz.timezone(settings.TIMEZONE)): + + # Retrieve necessary information from database + ticket_template = installation.ticket_template + + # Initialize blinking task + blinking_task = None + # Set Output to False + devices.OUTPUT.set(True) + + # Take a snapshot + start = time.time() + snapshot = devices.CAMERA.capture(installation.id) + end = time.time() + logger.info('Snapshot capture successfully executed in %s seconds', end - start) + # Start blinking + blinking_task = devices.OUTPUT.blink() + + # Render ticket + start = time.time() + code = installation.get_code() + renderer = TicketRenderer(ticket_template['html'], + ticket_template['text_variables'], + ticket_template['image_variables'], + ticket_template['images']) + html, dt, code, random_text_selections, random_image_selections = \ + renderer.render(snapshot, code) + with codecs.open(settings.TICKET_HTML_PATH, 'w', 'utf-8') as ticket: + ticket.write(html) + url = "file://%s" % settings.TICKET_HTML_PATH + phantom_js.get(url) + ticket = join(settings.TICKET_DIR, basename(snapshot)) + phantom_js.save_screenshot(ticket) + end = time.time() + logger.info('Ticket successfully rendered in %s seconds', end - start) + + # Print ticket + start = time.time() + devices.PRINTER.print_ticket(ticket) + end = time.time() + logger.info('Ticket successfully printed in %s seconds', end - start) + + # Stop blinking + blinking_task.terminate() + + # Set Output to True + devices.OUTPUT.set(False) + + # add task upload ticket task to the queue + ticket = { + 'installation': installation.id, + 'snapshot': snapshot, + 'ticket': ticket, + 'dt': dt, + 'code': code, + 'random_text_selections': random_text_selections, + 'random_image_selections': random_image_selections + } + db.dbroot['tickets'].add_ticket(ticket) + else: + logger.warning("Current installation has ended. Skipping processus execution") except Exception as e: - logger.error(e.message) - logger.error(traceback.format_exc()) + logger.exception(e) finally: if 'blinking_task' in locals(): if blinking_task is not None: diff --git a/figureraspbian/settings.py b/figureraspbian/settings.py index 8d4d90e..e6dd6c5 100644 --- a/figureraspbian/settings.py +++ b/figureraspbian/settings.py @@ -83,10 +83,6 @@ def get_env_setting(setting, default=None): flash_on = get_env_setting('FLASH_ON', '0') FLASH_ON = True if flash_on == '1' else False -# Ping adress -PING_ADDRESS = get_env_setting('PING_ADDRESS', 'https://api.figuredevices.com') - - def log_config(): logger.info('RESIN_DEVICE_UUID: %s' % RESIN_DEVICE_UUID) logger.info('ENVIRONMENT: %s' % ENVIRONMENT) diff --git a/figureraspbian/tasks.py b/figureraspbian/tasks.py index ded5705..5243f28 100644 --- a/figureraspbian/tasks.py +++ b/figureraspbian/tasks.py @@ -20,15 +20,16 @@ from django.core.cache import cache from celery import Celery -from .utils import internet_on from .db import Database, managed -from . import api - app = Celery('tasks', broker='amqp://guest@localhost//') app.conf.update( CELERYBEAT_SCHEDULE={ + 'upload-ticket-every-minute-and-half': { + 'task': 'figureraspbian.tasks.upload_tickets', + 'schedule': timedelta(seconds=90) + }, 'update-db-every-minute': { 'task': 'figureraspbian.tasks.update_db', 'schedule': timedelta(seconds=60) @@ -55,19 +56,15 @@ def wrapper(*args, **kwargs): return task_exc - -@app.task(rate_limit='10/m') -def create_ticket(ticket): - if internet_on(): - api.create_ticket(ticket) - else: - create_ticket.apply_async( - ticket, - countdown=settings.RETRY_DELAY) +@single_instance_task(60 * 10) +@app.task +def upload_tickets(): + """ Upload all tickets that have not been previously updated""" + with managed(Database()) as db: + db.dbroot['tickets'].upload_tickets() @app.task def update_db(): - if internet_on(): - with managed(Database()) as db: - db.update() \ No newline at end of file + with managed(Database()) as db: + db.dbroot['installation'].update() \ No newline at end of file diff --git a/figureraspbian/tests.py b/figureraspbian/tests.py index 9bc31b8..8bd2060 100644 --- a/figureraspbian/tests.py +++ b/figureraspbian/tests.py @@ -9,13 +9,14 @@ from .utils import url2name from .db import Database, NotInitializedError, managed from . import api, settings, processus -from mock import MagicMock +from mock import MagicMock, Mock +import transaction +import urllib2 class TestTicketRenderer(unittest.TestCase): def setUp(self): - installation = '1' html = '{{snapshot}} {{code}} {{datetime | datetimeformat}} {{textvariable_1}} {{imagevariable_2}} {{image_1}}' self.chiefs = ['Titi', 'Vicky', 'Benni'] self.chiefs = [{'id': '1', 'text': 'Titi'}, {'id': '2', 'text': 'Vicky'}, {'id': '3', 'text': 'Benni'}] @@ -37,21 +38,13 @@ def test_random_selection(self): self.assertEqual(random_image_selections[0][0], '2') self.assertTrue(random_image_selections[0][1] in self.paths) - def test_code(self): - """ - generics function should return a proper code - """ - _, code = self.ticket_renderer.generics('1') - self.assertTrue(len(code) == 8) - self.assertTrue(code.isupper()) - def test_render(self): """ TicketRenderer should render a ticket """ - rendered_html, _, _, _, _ = self.ticket_renderer.render('1', '/path/to/snapshot') - expected = re.compile("/path/to/snapshot \w{8} \d{4}-\d{2}-\d{2} (Titi|Vicky|Benni) /path/to/variable/image(1|2) path/to/image") - self.assertRegexpMatches(rendered_html, expected) + code = '5KIJ7' + rendered_html, _, _, _, _ = self.ticket_renderer.render('/path/to/snapshot', code) + print rendered_html def test_set_date_format(self): """ @@ -59,19 +52,27 @@ def test_set_date_format(self): """ html = '{{datetime | datetimeformat("%Y")}}' self.ticket_renderer.html = html - rendered_html, _, _, _, _ = self.ticket_renderer.render('1', '/path/to/snapshot') + rendered_html, _, _, _, _ = self.ticket_renderer.render('/path/to/snapshot', '00000') self.assertRegexpMatches(rendered_html, re.compile("\d{4}")) def test_encode_non_unicode_character(self): """ Ticket renderer should encode non unicode character """ - html = "Du texte avec un accent ici: é" + html = u"Du texte avec un accent ici: é" self.ticket_renderer.html = html - rendered_html, _, _, _, _ = self.ticket_renderer.render('1', '/path/to/snapshot') - print rendered_html + rendered_html, _, _, _, _ = self.ticket_renderer.render('/path/to/snapshot', '00000') self.assertTrue(u'Du texte avec un accent ici: é' in rendered_html) + def test_render_multiple_times(self): + """ + Ticket renderer should render tickets multiples times with different codes + """ + rendered1 = self.ticket_renderer.render('/path/to/snapshot', '00000') + rendered2 = self.ticket_renderer.render('/path/to/snapshot', '00001') + self.assertIn('00000', rendered1) + self.assertIn('00001', rendered2) + class TestUtilityFunction(unittest.TestCase): @@ -155,173 +156,269 @@ class TestDatabase(unittest.TestCase): def setUp(self): self.mock_installation = { - "scenario_obj": { - "id": 1, + "scenario": { + "name": "Marabouts", + "ticket_template": { + "html": "", + "text_variables": [ + { + "owner": "test@figuredevices.com", + "id": 1, + "name": "Profession", + "items": [ + { + "owner": "test@figuredevices.com", + "id": 1, + "text": "Professeur", + "variable": 1 + }, + { + "owner": "test@figuredevices.com", + "id": 2, + "text": "Monsieur", + "variable": 1 + } + ] + } + ], + "image_variables": [ + { + "owner": "test@figuredevices.com", + "id": 1, + "name": "animaux", + "items": [ + { + "id": 2, + "media": "http://api-integration.figuredevices.com/media/images/1427817820717.jpg", + "variable": 1 + } + ] + } + ], + "images": [ + { + "id": 2, + "media": "http://api-integration.figuredevices.com/media/images/1427817820718.jpg", + "variable": None + } + ] + } }, "place": None, "start": "2016-07-01T12:00:00Z", - "end": "2016-07-02T12:00:00Z" - } - - self.mock_scenario = { - "name": "Marabouts", - "ticket_template": { - "html": "", - "text_variables_objects": [ - { - "owner": "test@figuredevices.com", - "id": 1, - "name": "Profession", - "items": [ - { - "owner": "test@figuredevices.com", - "id": 1, - "text": "Professeur", - "variable": 1 - }, - { - "owner": "test@figuredevices.com", - "id": 2, - "text": "Monsieur", - "variable": 1 - } - ] - } - ], - "image_variables_objects": [ - { - "owner": "test@figuredevices.com", - "id": 1, - "name": "animaux", - "items": [ - { - "id": 2, - "media": "http://api-integration.figuredevices.com/media/images/1427817820717.jpg", - "variable": 1 - } - ] - } - ], - "images_objects": [ - { - "id": 2, - "media": "http://api-integration.figuredevices.com/media/images/1427817820718.jpg", - "variable": None - } - ] - } + "end": "2016-07-02T12:00:00Z", + "id": "1" } + self.mock_codes = ['25JHU', '54KJI', 'KJ589', 'KJ78I', 'JIKO5'] + with managed(Database()) as db: + db.dbroot.clear() + transaction.commit() def test_initialization(self): """ - Database should not be initialized when first created + Database should be initialized when first created """ - database = Database('development') + api.download = MagicMock() + api.get_installation = MagicMock(return_value=self.mock_installation) + api.get_codes = MagicMock(return_value=self.mock_codes) + database = Database() with managed(database) as db: - self.assertFalse(db.is_initialized()) - db.clear() + self.assertIn('installation', db.dbroot) + installation = db.dbroot['installation'] + self.assertEqual(installation.id, "1") + self.assertIsNotNone(installation.start) + self.assertIsNotNone(installation.end) + self.assertEqual(installation.scenario['name'], 'Marabouts') + self.assertIsNotNone(installation.ticket_template) + self.assertIn('tickets', db.dbroot) + self.assertEqual(installation.codes, self.mock_codes) - def test_raise_not_initialized_error(self): + def test_second_connection(self): """ - Database should raise exception when not initialized and trying to access data + installation should not be updated on second connection """ - database = Database('development') - with managed(database) as db: - with self.assertRaises(NotInitializedError): - db.installation() - db.clear() + api.download = MagicMock() + api.get_installation = MagicMock(return_value=self.mock_installation) + api.get_codes = MagicMock(return_value=self.mock_codes) + with managed(Database()): + pass + api.download = MagicMock() + api.get_installation = MagicMock(return_value=self.mock_installation) + api.get_codes = MagicMock(return_value=self.mock_codes) + with managed(Database()): + assert not api.download.called + assert not api.get_installation.called + assert not api.get_codes.called - def test_update(self): + def test_get_installation_return_none(self): """ - Database should update + Installation should not be initialized if api return None """ - api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) - api.download = MagicMock() - database = Database('development') + api.get_installation = MagicMock(return_value=None) + api.get_codes = MagicMock(return_value=self.mock_codes) + database = Database() with managed(database) as db: - db.update() - api.get_scenario.assert_called_with(1) - self.assertTrue(db.is_initialized()) - db.clear() + self.assertIn('installation', db.dbroot) + self.assertIsNone(db.dbroot['installation'].id) - def test_installation(self): + def test_get_installation_raise_exception(self): """ - Database should return installation + Installation should not be initialized if get_installation raise Exception """ - api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) - api.download = MagicMock() - database = Database('development') + api.get_installation = Mock(side_effect=api.ApiException) + api.get_codes = MagicMock(return_value=self.mock_codes) + database = Database() with managed(database) as db: - db.update() - self.assertIsNotNone(db.installation()) - db.clear() + self.assertIn('installation', db.dbroot) + self.assertIsNone(db.dbroot['installation'].id) - def test_scenario(self): + def test_get_codes_raise_exception(self): """ - Database should return scenario + Installation should not be initialized if get_codes raise Exception """ api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) - api.download = MagicMock() - database = Database('development') + api.get_codes = Mock(side_effect=api.ApiException) + database = Database() with managed(database) as db: - db.update() - self.assertIsNotNone(db.scenario()) - db.clear() + self.assertIn('installation', db.dbroot) + self.assertIsNone(db.dbroot['installation'].id) - def test_ticket_template(self): + def test_download_raise_exception(self): """ - Database should return ticket template + Transaction should abort """ + api.download = Mock(side_effect=urllib2.HTTPError('', '', '', '', None)) + api.get_codes = MagicMock(return_value=self.mock_codes) api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) - api.download = MagicMock() - database = Database('development') + database = Database() with managed(database) as db: - db.update() - self.assertIsNotNone(db.ticket_template()) - db.clear() + self.assertIn('installation', db.dbroot) + self.assertIsNone(db.dbroot['installation'].id) - def test_text_variables(self): + def test_update_installation(self): """ - Database should return text variables + Installation should update after initialization """ + api.download = MagicMock() api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) + api.get_codes = MagicMock(return_value=self.mock_codes) + with managed(Database()): + pass + with managed(Database()) as db: + api.get_installation = MagicMock(return_value=None) + api.get_codes = MagicMock(return_value=None) + db.dbroot['installation'].update() + self.assertIsNone(db.dbroot['installation'].id) + with managed(Database()) as db: + self.assertIsNone(db.dbroot['installation'].id) + + def test_installation_does_not_change(self): + """ + api.get_codes should not be called if installation.id does not change + """ api.download = MagicMock() - database = Database('development') + api.get_installation = MagicMock(return_value=self.mock_installation) + api.get_codes = MagicMock(return_value=self.mock_codes) + database = Database() with managed(database) as db: - db.update() - self.assertTrue(len(db.text_variables()), 1) - db.clear() + api.get_codes = Mock(side_effect=Exception('this method should not be called')) + db.dbroot['installation'].update() + self.assertEqual(db.dbroot['installation'].codes, self.mock_codes) - def test_image_variables(self): + def test_installation_changes(self): """ - Database should return image variables + codes should be updated if installation id changes """ + api.download = MagicMock() api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) + api.get_codes = MagicMock(return_value=self.mock_codes) + database = Database() + with managed(database) as db: + changed = self.mock_installation + changed['id'] = '2' + new_codes = ['54JU5', 'JU598', 'KI598', 'KI568', 'JUI58'] + api.get_codes = MagicMock(return_value=new_codes) + db.dbroot['installation'].update() + self.assertEqual(db.dbroot['installation'].codes, new_codes) + + def test_get_code(self): api.download = MagicMock() - database = Database('development') - with managed(database)as db: - db.update() - self.assertTrue(len(db.image_variables()), 1) - db.clear() + api.get_installation = MagicMock(return_value=self.mock_installation) + api.get_codes = MagicMock(return_value=['00000', '00001']) + with managed(Database()) as db: + code = db.dbroot['installation'].get_code() + self.assertEqual(code, '00001') + self.assertEqual(db.dbroot['installation'].codes, ['00000']) + with managed(Database()) as db: + self.assertEqual(db.dbroot['installation'].codes, ['00000']) - def test_images(self): + def test_add_ticket(self): """ - Database should return images + TicketsGallery should add a ticket """ + api.download = MagicMock() api.get_installation = MagicMock(return_value=self.mock_installation) - api.get_scenario = MagicMock(return_value=self.mock_scenario) + api.get_codes = MagicMock(return_value=['00000', '00001']) + with managed(Database()) as db: + self.assertEqual(len(db.dbroot['tickets']._tickets), 0) + now = datetime.now(pytz.timezone(settings.TIMEZONE)) + ticket = { + 'installation': '1', + 'snapshot': '/path/to/snapshot', + 'ticket': 'path/to/ticket', + 'dt': now, + 'code': 'JHUYG', + 'random_text_selections': [], + 'random_image_selections': [], + } + db.dbroot['tickets'].add_ticket(ticket) + self.assertEqual(len(db.dbroot['tickets']._tickets), 1) + self.assertEqual(db.dbroot['tickets']._tickets[now], ticket) + self.assertIn('uploaded', db.dbroot['tickets']._tickets[now]) + with managed(Database()) as db: + self.assertEqual(len(db.dbroot['tickets']._tickets), 1) + + def test_upload_tickets(self): + """ + Uploading a ticket should upload all non uploaded tickets + """ api.download = MagicMock() - database = Database('development') - with managed(database) as db: - db.update() - self.assertTrue(len(db.images()), 1) - db.clear() + api.get_installation = MagicMock(return_value=self.mock_installation) + api.get_codes = MagicMock(return_value=['00000', '00001']) + api.create_ticket = MagicMock() + with managed(Database()) as db: + time1 = datetime.now(pytz.timezone(settings.TIMEZONE)) + time2 = datetime.now(pytz.timezone(settings.TIMEZONE)) + ticket_1 = { + 'installation': '1', + 'snapshot': '/path/to/snapshot', + 'ticket': 'path/to/ticket', + 'dt': time1, + 'code': 'JHUYG', + 'random_text_selections': [], + 'random_image_selections': [], + 'uploaded': False + } + ticket_2 = { + 'installation': '1', + 'snapshot': '/path/to/snapshot', + 'ticket': 'path/to/ticket', + 'dt': time2, + 'code': 'JU76G', + 'random_text_selections': [], + 'random_image_selections': [], + 'uploaded': True + } + db.dbroot['tickets'].add_ticket(ticket_1) + db.dbroot['tickets'].add_ticket(ticket_2) + db.dbroot['tickets'].upload_tickets() + api.create_ticket.assert_called_once_with(ticket_1) + # check the transaction is actually commited + with managed(Database()): + db.dbroot['tickets']._tickets[time1]['uploaded'] = True + api.create_ticket = MagicMock() + db.dbroot['tickets'].upload_tickets() + self.assertFalse(api.create_ticket.called) class TestProcessus(unittest.TestCase): diff --git a/figureraspbian/ticketrenderer.py b/figureraspbian/ticketrenderer.py index bf721f9..ec56af1 100644 --- a/figureraspbian/ticketrenderer.py +++ b/figureraspbian/ticketrenderer.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- import random -import time import os from datetime import datetime import pytz -from hashids import Hashids from jinja2 import Environment from . import settings @@ -20,18 +18,24 @@ def with_base_html(rendered): add html boilerplate to rendered template """ base = u""" - -
- - - - -