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""" - - - - - - -
- {content} -
- - - """ + + + + + + +
+ {content} +

+ + Tapez votre code sur figuredevices.com + + {{{{code}}}} + + +
+ +""" return base.format(content=rendered, ticket_css=settings.TICKET_CSS_PATH) @@ -75,34 +79,21 @@ def random_selection(self): image_variable in self.image_variables if len(image_variable['items']) > 0] return random_text_selections, random_image_selections - def generics(self, installation): - """ - Calculate generics variables like datetime, code. These variables are not randomly calculated but - deterministically calculated - :return: - """ - now = datetime.now(pytz.timezone(settings.TIMEZONE)) - epoch = int(time.mktime(now.timetuple()) - FIGURE_TIME_ORIGIN) - hashids = Hashids() - code = hashids.encode(epoch, int(installation)).upper() - return now, code - - def render(self, installation, snapshot): + def render(self, snapshot, code): context = {'snapshot': 'file://%s' % snapshot} (random_text_selections, random_image_selections) = self.random_selection() for (text_variable_id, item) in random_text_selections: - context['textvariable_%s' % text_variable_id] = item['text'] + context['textvariable_%s' % text_variable_id] = item['text'] for (image_variable_id, item) in random_image_selections: context['imagevariable_%s' % image_variable_id] = 'file://%s/%s' % (settings.IMAGE_DIR, os.path.basename(item['media'])) - now, code = self.generics(installation) + now = datetime.now(pytz.timezone(settings.TIMEZONE)) context['datetime'] = now context['code'] = code for im in self.images: context['image_%s' % im['id']] = 'file://%s/%s' % (settings.IMAGE_DIR, os.path.basename(im['media'])) - template = JINJA_ENV.from_string(self.html) + template = JINJA_ENV.from_string(with_base_html(self.html)) rendered_html = template.render(context) - rendered_html = with_base_html(rendered_html) return rendered_html, now, code, random_text_selections, random_image_selections diff --git a/figureraspbian/utils.py b/figureraspbian/utils.py index 5980bff..0523f21 100644 --- a/figureraspbian/utils.py +++ b/figureraspbian/utils.py @@ -4,31 +4,9 @@ import urllib from urlparse import urlsplit -import requests -from requests.exceptions import Timeout, ConnectionError - -from . import settings - - -def internet_on(): - """ - Check if our device has access to the internet - """ - try: - requests.get(settings.PING_ADDRESS, timeout=1) - return True - except Timeout: - pass - except ConnectionError: - pass - return False - - def url2name(url): """ Convert a file url to its base name http://api.figuredevices.com/static/css/ticket.css => ticket.css """ return basename(urllib.unquote(urlsplit(url)[2])) - - diff --git a/install-piface-real-time-clock.sh b/install-piface-real-time-clock.sh new file mode 100644 index 0000000..cfa46d0 --- /dev/null +++ b/install-piface-real-time-clock.sh @@ -0,0 +1,77 @@ +#!/bin/bash +#: Description: Enables the required modules for PiFace Clock. + +#======================================================================= +# NAME: set_revision_var +# DESCRIPTION: Stores the revision number of this Raspberry Pi into +# $RPI_REVISION +#======================================================================= +set_revision_var() { + rev_str=$(grep "Revision" /proc/cpuinfo) + # get the last character + len_rev_str=${#rev_str} + chr_index=$(($len_rev_str-1)) + chr=${rev_str:$chr_index:$len_rev_str} + if [[ $chr == "2" || $chr == "3" ]]; then + RPI_REVISION="1" + else + RPI_REVISION="2" + fi +} + +#======================================================================= +# NAME: enable_module +# DESCRIPTION: Enabled the I2C module. +#======================================================================= +enable_module() { + echo "Enabling I2C module." + module="i2c-bcm2708" + modules_file="/etc/modules" + # if $module not in $modules_file: append $module to $modules_file. + if ! grep -q $module $modules_file; then + echo $module >> $modules_file + fi +} + +#======================================================================= +# NAME: start_on_boot +# DESCRIPTION: Load the I2C modules and send magic number to RTC, on boot. +#======================================================================= +start_on_boot() { + echo "Changing /etc/rc.local to load time from PiFace Clock." + + # remove exit 0 + sed -i "s/exit 0//" /etc/rc.local + + if [[ $RPI_REVISION == "1" ]]; then + i=0 # i2c-0 + else + i=1 # i2c-1 + fi + + cat >> /etc/rc.local << EOF +modprobe i2c-dev +modprobe i2c:mcp7941x +echo mcp7941x 0x6f > /sys/class/i2c-dev/i2c-$i/device/new_device +hwclock -s +EOF +} + +#======================================================================= +# MAIN +#======================================================================= +# check if the script is being run as root +if [[ $EUID -ne 0 ]] +then + printf 'This script must be run as root.\nExiting..\n' + exit 1 +fi +RPI_REVISION="" +set_revision_var && +enable_module && +start_on_boot && +printf 'Please *reboot* and then set your clock with: + + sudo date -s "14 JAN 2014 10:10:30" + +' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b75f0d7..266e2f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ gphoto2==0.11.0 pifacecommon==4.1.2 pifacedigitalio==3.0.5 jinja2==2.7.3 -hashids==1.0.3 persistent==4.0.8 ZODB==4.1.0 ZODB3==3.11.0 diff --git a/start.sh b/start.sh index 8d3e30d..b19f592 100644 --- a/start.sh +++ b/start.sh @@ -3,8 +3,15 @@ # Enable I2C. See http://docs.resin.io/#/pages/i2c-and-spi.md for more details modprobe i2c-dev -# Mount USB storage -mount /dev/sda1 /mnt && chmod 775 /mnt +# create data directories +mkdir -p /data/tickets /data/images /data/snapshots /data/resources + +# Install Real Time Clock +chmod +x /install-piface-real-time-clock.sh +/install-piface-real-time-clock.sh +lsmod +hwclock -r +date # Launch supervisor in the foreground echo 'Starting supervisor' diff --git a/supervisord.conf b/supervisord.conf index 4adf899..855b1a8 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -33,7 +33,7 @@ redirect_stderr=true [program:zeo] process_name=zeo -command=runzeo -a /var/run/zeo.sock -f /var/db/db.fs +command=runzeo -a /var/run/zeo.sock -f /data/db.fs stdout_logfile=/var/log/zeo.log redirect_stderr=true