source: rattail/rattail/app.py @ f720a96

Last change on this file since f720a96 was f720a96, checked in by Lance Edgar <lance@…>, 11 months ago

Allow "default" batch handlers to be registered in config

so that the package actually providing them, can register defaults,
without setting value of *actual* registered handler

  • Property mode set to 100644
File size: 45.3 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2022 Lance Edgar
6#
7#  This file is part of Rattail.
8#
9#  Rattail is free software: you can redistribute it and/or modify it under the
10#  terms of the GNU General Public License as published by the Free Software
11#  Foundation, either version 3 of the License, or (at your option) any later
12#  version.
13#
14#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
15#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17#  details.
18#
19#  You should have received a copy of the GNU General Public License along with
20#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24App Handler
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import os
30# import re
31import datetime
32import shutil
33import tempfile
34import warnings
35import logging
36
37import six
38from mako.template import Template
39
40from rattail.util import (load_object, load_entry_points,
41                          OrderedDict, progress_loop,
42                          pretty_quantity, pretty_hours)
43from rattail.files import temp_path, resource_path
44from rattail.mail import send_email
45from rattail.config import parse_list
46from rattail.core import get_uuid, Object
47
48
49log = logging.getLogger(__name__)
50
51
52class AppHandler(object):
53    """
54    Base class and default implementation for top-level Rattail app handler.
55
56    aka. "the handler to handle all handlers"
57
58    aka. "one handler to bind them all"
59    """
60    default_autocompleters = {
61        'brands': 'rattail.autocomplete.brands:BrandAutocompleter',
62        'customers': 'rattail.autocomplete.customers:CustomerAutocompleter',
63        'customers.neworder': 'rattail.autocomplete.customers:CustomerNewOrderAutocompleter',
64        'customers.phone': 'rattail.autocomplete.customers:CustomerPhoneAutocompleter',
65        'employees': 'rattail.autocomplete.employees:EmployeeAutocompleter',
66        'departments': 'rattail.autocomplete.departments:DepartmentAutocompleter',
67        'people': 'rattail.autocomplete.people:PersonAutocompleter',
68        'people.employees': 'rattail.autocomplete.people:PersonEmployeeAutocompleter',
69        'people.neworder': 'rattail.autocomplete.people:PersonNewOrderAutocompleter',
70        'products': 'rattail.autocomplete.products:ProductAutocompleter',
71        'products.all': 'rattail.autocomplete.products:ProductAllAutocompleter',
72        'products.neworder': 'rattail.autocomplete.products:ProductNewOrderAutocompleter',
73        'vendors': 'rattail.autocomplete.vendors:VendorAutocompleter',
74    }
75
76    setting_utctime_format = '%Y-%m-%d %H:%M:%S'
77
78    def __init__(self, config):
79        self.config = config
80
81        # app may not use the db layer, but if so we set the model
82        try:
83            self.model = config.get_model()
84        except: # pragma: no cover
85            pass
86
87    def get_title(self, default=None):
88        """
89        Returns the configured title (name) of the app.
90
91        Default logic invokes
92        :meth:`rattail.config.RattailConfig.app_title()` to obtain the
93        title.
94
95        :param default: Value to be returned if there is no app title
96           configured.
97
98        :returns: Title for the app.
99        """
100        return self.config.app_title(default=default)
101
102    def get_timezone(self, key='default'):
103        """
104        Returns a configured time zone.
105
106        Default logic invokes :func:`rattail.time.timezone()` to
107        obtain the time zone object.
108
109        :param key: Unique key designating which time zone should be
110           returned.  Note that most apps have only one ("default"),
111           but may have others defined.
112        """
113        from rattail.time import timezone
114        return timezone(self.config, key)
115
116    def localtime(self, *args, **kwargs):
117        """
118        Produce or convert a timestamp in the default time zone.
119
120        Default logic invokes :func:`rattail.time.localtime()` to
121        obtain the timestamp.  All args and kwargs are passed directly
122        to that function.
123
124        :returns: A :class:`python:datetime.datetime` object.  Usually
125           this will be timezone-aware but this will depend on the
126           args and kwargs you specify.
127        """
128        from rattail.time import localtime
129        return localtime(self.config, *args, **kwargs)
130
131    def make_utc(self, *args, **kwargs):
132        """
133        Produce or convert a timestamp to UTC time zone.
134
135        Default logic invokes :func:`rattail.time.make_utc()` to
136        obtain the timestamp.  All args and kwargs are passed directly
137        to that function.
138
139        :returns: A :class:`python:datetime.datetime` object.  Usually
140           this will be timezone-naive but this will depend on the
141           args and kwargs you specify.
142        """
143        from rattail.time import make_utc
144        return make_utc(*args, **kwargs)
145
146    def load_entry_points(self, group, **kwargs): # pragma: no cover
147        warnings.warn("method is deprecated, please use "
148                      "util.load_entry_points() function directly",
149                      DeprecationWarning)
150        return load_entry_points(group, **kwargs)
151
152    def load_object(self, spec):
153        """
154        Import and/or load and return the object designated by the
155        given spec string.
156
157        Default logic invokes :func:`rattail.util.load_object()` to
158        obtain the object.
159
160        The syntax of the spec string is not unique to Rattail, but is
161        not a universal standard, so deserves a note here.  The spec
162        is basically just ``modulepath:objname`` where ``modulepath``
163        is the dotted name of the full module where the object
164        resides, and ``objname`` is the name of the object within that
165        module.  For instance, ``rattail.app:AppHandler`` would be the
166        spec for *this* class.
167
168        Note also that the use of the word "object" may be confusing,
169        as it does not signify an "instance" but rather an object in
170        the generic sense.  Most often a spec will either refer to a
171        class or function within the module, although any valid named
172        object is possible.
173
174        :param spec: String like ``modulepath:objname`` as described
175           above.
176
177        :returns: The object referred to by ``spec``.  If the module
178           could not be imported, or did not contain an object of the
179           given name, then an error will raise.
180        """
181        return load_object(spec)
182
183    def next_counter_value(self, session, key, **kwargs):
184        """
185        Return the next counter value for the given key.
186
187        :param session: Current session for Rattail DB.
188
189        :param key: Unique key indicating the counter for which the
190           next value should be fetched.
191
192        :returns: Next value as integer.
193        """
194        dialect = session.bind.url.get_dialect().name
195        if dialect != 'postgresql':
196            log.debug("non-postgresql database detected; will use workaround")
197            from rattail.db.util import CounterMagic
198            magic = CounterMagic(self.config)
199            return magic.next_value(session, key)
200
201        # normal (uses postgresql sequence)
202        sql = "select nextval('{}_seq')".format(key)
203        value = session.execute(sql).scalar()
204        return value
205
206    def get_setting(self, session, name, typ=None, **kwargs):
207        model = self.model
208        setting = session.query(model.Setting).get(name)
209        value = None if setting is None else setting.value
210
211        if typ == 'utctime':
212            if value:
213                value = datetime.datetime.strptime(value, self.setting_utctime_format)
214            else:
215                value = None
216
217        return value
218
219    def save_setting(self, session, name, value, typ=None, **kwargs):
220        model = self.model
221
222        if typ == 'utctime':
223            if value:
224                value = value.strftime(self.setting_utctime_format)
225            else:
226                value = None
227
228        setting = session.query(model.Setting).get(name)
229        if not setting:
230            setting = model.Setting(name=name)
231            session.add(setting)
232        setting.value = value
233
234    def delete_setting(self, session, name, **kwargs):
235        model = self.model
236        setting = session.query(model.Setting).get(name)
237        if setting:
238            session.delete(setting)
239
240    def get_active_stores(self, session, **kwargs):
241        """
242        Returns the list of "active" stores.  A store is considered
243        active if it is *not* marked as archived.
244
245        :param session: Reference to current DB session.
246
247        :returns: Possibly-empty list of
248           :class:`~rattail.db.model.stores.Store` records which are
249           deemed active.
250        """
251        import sqlalchemy as sa
252
253        model = self.model
254        return session.query(model.Store)\
255                      .filter(sa.or_(
256                          model.Store.archived == False,
257                          model.Store.archived == None))\
258                      .order_by(model.Store.id)\
259                      .all()
260
261    def get_autocompleter(self, key, **kwargs):
262        """
263        Returns a new :class:`~rattail.autocomplete.base.Autocompleter`
264        instance corresponding to the given key, e.g. ``'products'``.
265
266        The app handler has some hard-coded defaults for the built-in
267        autocompleters (see ``default_autocompleters`` in the source
268        code).  You can override any of these, and/or add your own
269        with custom keys, via config, e.g.:
270
271        .. code-block:: ini
272
273           [rattail]
274           autocomplete.products = poser.autocomplete.products:ProductAutocompleter
275           autocomplete.otherthings = poser.autocomplete.things:OtherThingAutocompleter
276
277        With the above you can then fetch your custom autocompleter with::
278
279           autocompleter = app.get_autocompleter('otherthings')
280
281        In any case if it can locate the class, it will create an
282        instance of it and return that.
283
284        :params key: Unique key for the type of autocompleter you
285           need.  Often is a simple string, e.g. ``'customers'`` but
286           sometimes there may be a "modifier" with it to get an
287           autocompleter with more specific behavior.
288
289           For instance ``'customers.phone'`` would effectively give
290           you a customer autocompleter but which searched by phone
291           number instead of customer name.
292
293           Note that each key is still a simple string though, and that
294           must be "unique" in the sense that only one autocompleter
295           can be configured for each key.
296
297        :returns: An :class:`~rattail.autocomplete.base.Autocompleter`
298           instance if found, otherwise ``None``.
299        """
300        spec = self.config.get('rattail', 'autocomplete.{}'.format(key))
301        if not spec:
302            spec = self.default_autocompleters.get(key)
303        if spec:
304            return load_object(spec)(self.config)
305
306        raise ValueError("cannot locate autocompleter for key: {}".format(key))
307
308    def get_auth_handler(self, **kwargs):
309        """
310        Get the configured "auth" handler.
311
312        :returns: The :class:`~rattail.auth.AuthHandler` instance for
313           the app.
314        """
315        if not hasattr(self, 'auth_handler'):
316            spec = self.config.get('rattail', 'auth.handler',
317                                   default='rattail.auth:AuthHandler')
318            factory = load_object(spec)
319            self.auth_handler = factory(self.config, **kwargs)
320        return self.auth_handler
321
322    def get_batch_handler(self, key, default=None, error=True, **kwargs):
323        """
324        Get the configured batch handler of the given type.
325
326        :param key: Unique key designating which type of batch handler
327           is being requested.
328
329        :param default: Spec string to be used as the default, if no
330           handler is configured for the given batch type.  This spec
331           string must itself refer to a ``BatchHandler`` class.
332
333        :param error: Flag indicating whether an error should be
334           raised if no handler can be found.
335
336        :returns: A :class:`~rattail.batch.handlers.BatchHandler`
337           instance of the requested type.  If no such handler can be
338           found, and the ``error`` param is false, then ``None`` is
339           returned; otherwise an error will raise.
340        """
341        # spec is assumed to come from config/settings if present,
342        # otherwise caller-supplied default is assumed
343        spec = self.config.get('rattail.batch', '{}.handler'.format(key),
344                               default=default)
345        if not spec:
346            spec = self.config.get('rattail.batch', '{}.handler.default'.format(key))
347
348        # TODO: this probably should go away?
349        # if none of the above gave us a spec, check for common 'importer' type
350        if not spec and key == 'importer':
351            spec = 'rattail.batch.importer:ImporterBatchHandler'
352
353        if spec:
354            Handler = self.load_object(spec)
355            return Handler(self.config)
356
357        if error:
358            raise ValueError("handler spec not found for batch "
359                             "type: {}".format(key))
360
361    def get_board_handler(self, **kwargs):
362        """
363        Get the configured "board" handler.
364
365        :returns: The :class:`~rattail.board.BoardHandler` instance
366           for the app.
367        """
368        if not hasattr(self, 'board_handler'):
369            from rattail.board import get_board_handler
370            self.board_handler = get_board_handler(self.config, **kwargs)
371        return self.board_handler
372
373    def get_bounce_handler(self, key, **kwargs):
374        """
375        Get the configured email bounce handler of the given type.
376
377        :param key: Unique key designating which type of bounce
378           handler is being requested.
379
380        :returns: A :class:`~rattail.bouncer.handler.BounceHandler`
381           instance of the requested type.  If no such handler can be
382           found, an error will raise.
383        """
384        if not hasattr(self, 'bounce_handlers'):
385            self.bounce_handlers = {}
386
387        if key not in self.bounce_handlers:
388            spec = self.config.get('rattail.bouncer',
389                                   '{}.handler'.format(key))
390            if not spec and key == 'default':
391                spec = 'rattail.bouncer:BounceHandler'
392            if not spec:
393                raise ValueError("bounce handler spec not found for "
394                                 "type: {}".format(key))
395
396            Handler = self.load_object(spec)
397            self.bounce_handlers[key] = Handler(self.config, key)
398
399        return self.bounce_handlers[key]
400
401    def get_clientele_handler(self, **kwargs):
402        """
403        Get the configured "clientele" handler.
404
405        :returns: The :class:`~rattail.clientele.ClienteleHandler`
406           instance for the app.
407        """
408        if not hasattr(self, 'clientele_handler'):
409            from rattail.clientele import get_clientele_handler
410            self.clientele_handler = get_clientele_handler(self.config, **kwargs)
411        return self.clientele_handler
412
413    def get_custorder_handler(self, **kwargs):
414        """
415        Get the configured "customer order" handler.
416
417        :returns: The :class:`~rattail.custorders.CustomerOrderHandler`
418           instance for the app.
419        """
420        if not hasattr(self, 'custorder_handler'):
421            spec = self.config.get('rattail', 'custorders.handler',
422                                   default='rattail.custorders:CustomerOrderHandler')
423            Handler = self.load_object(spec)
424            self.custorder_handler = Handler(self.config)
425        return self.custorder_handler
426
427    def get_employment_handler(self, **kwargs):
428        """
429        Get the configured "employment" handler.
430
431        :returns: The :class:`~rattail.employment.EmploymentHandler`
432           instance for the app.
433        """
434        if not hasattr(self, 'employment_handler'):
435            from rattail.employment import get_employment_handler
436            self.employment_handler = get_employment_handler(self.config, **kwargs)
437        return self.employment_handler
438
439    def get_feature_handler(self, **kwargs):
440        """
441        Get the configured "feature" handler.
442
443        :returns: The :class:`~rattail.features.handlers.FeatureHandler`
444           instance for the app.
445        """
446        if not hasattr(self, 'feature_handler'):
447            from rattail.features import FeatureHandler
448            self.feature_handler = FeatureHandler(self.config, **kwargs)
449        return self.feature_handler
450
451    def get_email_handler(self, **kwargs):
452        """
453        Get the configured "email" handler.
454
455        :returns: The :class:`~rattail.mail.EmailHandler` instance for
456           the app.
457        """
458        if not hasattr(self, 'email_handler'):
459            from rattail.mail import get_email_handler
460            self.email_handler = get_email_handler(self.config, **kwargs)
461        return self.email_handler
462
463    def get_mail_handler(self, **kwargs): # pragma: no cover
464        warnings.warn("method is deprecated, please use "
465                      "AppHandler.get_email_handler() instead",
466                      DeprecationWarning)
467        return self.get_email_handler(**kwargs)
468
469    def get_all_import_handlers(self, ignore_errors=True, sort=False,
470                                **kwargs):
471        """
472        Returns *all* Import/Export Handler classes which are known to
473        exist, i.e.  all which are registered via ``setup.py`` for the
474        various packages installed.
475
476        This means it will include both "designated" handlers as well
477        as non-designated.  See
478        :meth:`get_designated_import_handlers()` if you only want the
479        designated ones.
480
481        Note that this will return the *Handler classes* and not
482        *handler instances*.
483
484        :param ignore_errors: Normally any errors which come up during
485           the loading process are ignored.  Pass ``False`` here to
486           force errors to raise, e.g. if you are not seeing the
487           results you expect.
488
489        :param sort: If you like the results can be sorted with a
490           simple key based on "Source -> Target" labels.
491
492        :returns: List of all registered Import/Export Handler classes.
493        """
494        # first load all "registered" Handler classes
495        Handlers = load_entry_points('rattail.importing',
496                                     ignore_errors=ignore_errors)
497
498        # organize registered classes by spec
499        specs = {}
500        for Handler in six.itervalues(Handlers):
501            spec = '{}:{}'.format(Handler.__module__, Handler.__name__)
502            specs[spec] = Handler
503
504        # many handlers may not be registered per se, but may be
505        # designated via config.  so try to include those too
506        for Handler in six.itervalues(Handlers):
507            spec = self.get_designated_import_handler_spec(Handler.get_key())
508            if spec and spec not in specs:
509                specs[spec] = load_object(spec)
510
511        # flatten back to simple list
512        Handlers = list(specs.values())
513
514        if sort:
515            Handlers.sort(key=lambda h: (h.get_generic_host_title(),
516                                         h.get_generic_local_title()))
517
518        return Handlers
519
520    def get_designated_import_handlers(self, with_alternates=False, **kwargs):
521        """
522        Returns all "designated" import/export handler instances.
523
524        Each "handler type key" can have at most one Handler class
525        which is "designated" in the config.  This method collects all
526        registered handlers and then sorts out which one is
527        designated, for each type key, ultimately returning only the
528        designated ones.
529
530        Note that this will return the *handler instances* and not
531        *Handler classes*.
532
533        If you have a type key and just need its designated handler,
534        see :meth:`get_import_handler()`.
535
536        See also :meth:`get_all_import_handlers()` if you need all
537        registered Handler classes.
538
539        :param with_alternates: If you specify ``True`` here then each
540           designated handler returned will have an extra attribute
541           named ``alternate_handlers``, which will be a list of the
542           other "available" (registered) handlers which match the
543           designated handler's type key.
544
545           This is probably most / only useful for the Configuration
546           UI, to allow admin to change which is designated.
547
548        :returns: List of all designated import/export handler instances.
549        """
550        grouped = OrderedDict()
551        for Handler in self.get_all_import_handlers(**kwargs):
552            key = Handler.get_key()
553            grouped.setdefault(key, []).append(Handler)
554
555        def find_designated(key, group):
556            spec = self.get_designated_import_handler_spec(key)
557            if spec:
558                for Handler in group:
559                    if Handler.get_spec() == spec:
560                        return Handler
561
562            if len(group) == 1:
563                return group[0]
564
565        designated = []
566        for key, group in six.iteritems(grouped):
567            Handler = find_designated(key, group)
568            if Handler:
569                # nb. we must instantiate here b/c otherwise if we
570                # assign the `alternate_handlers` attr onto the class,
571                # it can affect subclasses as well.  not so with
572                # instances though
573                handler = Handler(self.config)
574                if with_alternates:
575                    handler.alternate_handlers = [H for H in group
576                                                  if H is not Handler]
577                designated.append(handler)
578
579        return designated
580
581    def get_import_handler(self, key, require=False, **kwargs):
582        """
583        Return the designated import/export handler instance, per the
584        given handler type key.
585
586        See also :meth:`get_designated_import_handlers()` if you want
587        the full set of designated handlers.
588
589        :param key: A "handler type key", e.g.
590           ``'to_rattail.from_rattail.import'``.
591
592        :param require: Specify ``True`` here if you want an error to
593           be raised should no handler be found.
594
595        :returns: The import/export handler instance corresponding to
596           the given key.  If no handler can be found, then ``None``
597           is returned, unless ``require`` param is true, in which
598           case error is raised.
599        """
600        # first try to fetch the handler per designated spec
601        spec = self.get_designated_import_handler_spec(key, **kwargs)
602        if spec:
603            Handler = self.load_object(spec)
604            return Handler(self.config)
605
606        # nothing was designated, so leverage logic which already
607        # sorts out which handler is "designated" for given key
608        designated = self.get_designated_import_handlers()
609        for handler in designated:
610            if handler.get_key() == key:
611                return handler
612
613        if require:
614            raise ValueError("Cannot locate handler for key: {}".format(key))
615
616    def get_designated_import_handler(self, *args, **kwargs): # pragma: no cover
617        warnings.warn("method is deprecated, please use "
618                      "AppHandler.get_import_handler() instead",
619                      DeprecationWarning)
620        return self.get_import_handler(*args, **kwargs)
621
622    def get_designated_import_handler_spec(self, key, require=False, **kwargs):
623        """
624        Return the designated import handler "spec" string for the
625        given type key.
626
627        :param key: Unique key indicating the type of import handler.
628
629        :require: Flag indicating whether an error should be raised if no
630           handler is found.
631
632        :returns: Spec string for the designated handler.  If none is
633           found, then ``None`` is returned *unless* the ``require``
634           param is true, in which case an error is raised.
635        """
636        spec = self.config.get('rattail.importing',
637                               '{}.handler'.format(key))
638        if spec:
639            return spec
640
641        legacy_setting = self.config.get('rattail.importing',
642                                         '{}.legacy_handler_setting'.format(key))
643        if legacy_setting:
644            legacy_setting = parse_list(legacy_setting)
645            if len(legacy_setting) == 2:
646                section, option = legacy_setting
647                spec = self.config.get(section, option)
648                if spec:
649                    return spec
650
651        spec = self.config.get('rattail.importing',
652                               '{}.default_handler'.format(key))
653        if spec:
654            return spec
655
656        if require:
657            raise ValueError("Cannot locate handler spec for key: {}".format(key))
658
659    def get_label_handler(self, **kwargs):
660        """
661        Get the configured "label" handler.
662
663        See also :doc:`rattail-manual:base/handlers/other/labels`.
664
665        :returns: The :class:`~rattail.labels.LabelHandler` instance
666           for the app.
667        """
668        if not hasattr(self, 'label_handler'):
669            spec = self.config.get('rattail', 'labels.handler',
670                                   default='rattail.labels:LabelHandler')
671            factory = self.load_object(spec)
672            self.label_handler = factory(self.config, **kwargs)
673        return self.label_handler
674
675    def get_membership_handler(self, **kwargs):
676        """
677        Get the configured "membership" handler.
678
679        See also :doc:`rattail-manual:base/handlers/other/membership`.
680
681        :returns: The :class:`~rattail.membership.MembershipHandler`
682           instance for the app.
683        """
684        if not hasattr(self, 'membership_handler'):
685            spec = self.config.get('rattail', 'membership.handler',
686                                   default='rattail.membership:MembershipHandler')
687            factory = load_object(spec)
688            self.membership_handler = factory(self.config, **kwargs)
689        return self.membership_handler
690
691    def get_people_handler(self, **kwargs):
692        """
693        Get the configured "people" handler.
694
695        See also :doc:`rattail-manual:base/handlers/other/people`.
696
697        :returns: The :class:`~rattail.people.PeopleHandler` instance
698           for the app.
699        """
700        if not hasattr(self, 'people_handler'):
701            spec = self.config.get('rattail', 'people.handler',
702                                   default='rattail.people:PeopleHandler')
703            factory = load_object(spec)
704            self.people_handler = factory(self.config, **kwargs)
705        return self.people_handler
706
707    def get_poser_handler(self, **kwargs):
708        """
709        Get the configured "poser" handler.
710
711        :returns: The :class:`~rattail.poser.PoserHandler` instance
712           for the app.
713        """
714        if not hasattr(self, 'poser_handler'):
715            spec = self.config.get('rattail', 'poser.handler',
716                                   default='rattail.poser:PoserHandler')
717            factory = self.load_object(spec)
718            self.poser_handler = factory(self.config, **kwargs)
719        return self.poser_handler
720
721    def get_products_handler(self, **kwargs):
722        """
723        Get the configured "products" handler.
724
725        :returns: The :class:`~rattail.products.ProductsHandler`
726           instance for the app.
727        """
728        if not hasattr(self, 'products_handler'):
729            from rattail.products import get_products_handler
730            self.products_handler = get_products_handler(self.config, **kwargs)
731        return self.products_handler
732
733    def get_report_handler(self, **kwargs):
734        """
735        Get the configured "reports" handler.
736
737        :returns: The :class:`~rattail.reporting.handlers.ReportHandler`
738           instance for the app.
739        """
740        if not hasattr(self, 'report_handler'):
741            from rattail.reporting import get_report_handler
742            self.report_handler = get_report_handler(self.config, **kwargs)
743        return self.report_handler
744
745    def get_problem_report_handler(self, **kwargs):
746        """
747        Get the configured "problem reports" handler.
748
749        :returns: The :class:`~rattail.problems.handlers.ProblemReportHandler`
750           instance for the app.
751        """
752        if not hasattr(self, 'problem_report_handler'):
753            from rattail.problems import get_problem_report_handler
754            self.problem_report_handler = get_problem_report_handler(
755                self.config, **kwargs)
756        return self.problem_report_handler
757
758    def get_trainwreck_handler(self, **kwargs):
759        """
760        Get the configured "trainwreck" handler.
761
762        :returns: The :class:`~rattail.trainwreck.handler.TrainwreckHandler`
763           instance for the app.
764        """
765        if not hasattr(self, 'trainwreck_handler'):
766            spec = self.config.get('trainwreck', 'handler',
767                                   default='rattail.trainwreck.handler:TrainwreckHandler')
768            Handler = self.load_object(spec)
769            self.trainwreck_handler = Handler(self.config)
770        return self.trainwreck_handler
771
772    def get_vendor_handler(self, **kwargs):
773        """
774        Get the configured "vendor" handler.
775
776        :returns: The :class:`~rattail.vendors.handler.VendorHandler`
777           instance for the app.
778        """
779        if not hasattr(self, 'vendor_handler'):
780            spec = self.config.get('rattail', 'vendors.handler',
781                                   default='rattail.vendors:VendorHandler')
782            factory = self.load_object(spec)
783            self.vendor_handler = factory(self.config, **kwargs)
784        return self.vendor_handler
785
786    def progress_loop(self, *args, **kwargs):
787        """
788        Run a given function for a given sequence, and optionally show
789        a progress indicator.
790
791        Default logic invokes the :func:`rattail.util.progress_loop()`
792        function; see that for more details.
793        """
794        return progress_loop(*args, **kwargs)
795
796    def make_object(self, **kwargs):
797        """
798        Create and return a generic object.  All kwargs will be
799        assigned as attributes to the object.
800        """
801        return Object(**kwargs)
802
803    def get_session(self, obj):
804        """
805        Returns the SQLAlchemy session with which the given object is
806        associated.  Simple convenience wrapper around
807        :func:`sqlalchemy:sqlalchemy.orm.object_session()`.
808        """
809        from sqlalchemy import orm
810
811        return orm.object_session(obj)
812
813    def make_session(self, user=None, **kwargs):
814        """
815        Creates and returns a new SQLAlchemy session for the Rattail DB.
816
817        :param user: A "user-ish" object which should be considered
818           responsible for changes made during the session.  Can be
819           either a :class:`~rattail.db.model.users.User` object, or
820           just a (string) username.  If none is specified then the
821           config will be consulted for a default.
822
823        :returns: A :class:`rattail.db.Session` instance.
824        """
825        from rattail.db import Session
826
827        # always try to set default continuum user if possible
828        if 'continuum_user' not in kwargs:
829            if not user:
830                user = self.config.get('rattail', 'runas.default')
831            if user:
832                kwargs['continuum_user'] = user
833
834        return Session(**kwargs)
835
836    def cache_model(self, session, model, **kwargs):
837        """
838        Convenience method which invokes
839        :func:`rattail.db.cache.cache_model()` with the given model
840        and keyword arguments.
841        """
842        from rattail.db import cache
843        return cache.cache_model(session, model, **kwargs)
844
845    def make_appdir(self, path, **kwargs):
846        """
847        Establish an appdir at the given path.  This is really only
848        creating some folders so should be safe to run multiple times
849        even if the appdir already exists.
850        """
851        appdir = path
852        if not os.path.exists(appdir):
853            os.mkdir(appdir)
854
855        folders = [
856            'data',
857            os.path.join('data', 'uploads'),
858            'log',
859            'sessions',
860            'work',
861        ]
862        for name in folders:
863            path = os.path.join(appdir, name)
864            if not os.path.exists(path):
865                os.mkdir(path)
866
867    def render_mako_template(self, template_path, context,
868                             output_path=None, **kwargs):
869        """
870        Convenience method to render any (specified) Mako template.
871        """
872        output = Template(filename=template_path).render(**context)
873        if output_path:
874            with open(output_path, 'wt') as f:
875                f.write(output)
876        return output
877
878    def make_config_file(self, file_type, output_path, template_path=None,
879                         **kwargs):
880        """
881        Write a new config file of given type to specified location.
882
883        :param file_type: The "type" of config file to create.  This
884           is used to locate the file template, if ``template_path``
885           is not specified.  It also is used as default output
886           filename, if ``output_path`` is a folder.
887
888        :param output_path: Path to which new config file should be
889           written.  If this is a folder, then the filename is deduced
890           from the ``file_type``.
891
892        :param template_path: Optional path to config file template to
893           use.  If not specified, it will be looked up dynamically
894           based on the ``file_type``.  Note that the first template
895           found to match will be used.  Mako (``*.mako``) templates
896           are preferred, otherwise the template is assumed to be
897           "plain" and will be copied as-is to the output path.
898
899        :param **kwargs: Context to be passed to the Mako template, if
900           applicable.
901
902        :returns: Final path to which new config file was written.
903        """
904        # lookup template if not specified
905        if not template_path:
906            template_path = self.find_config_template(file_type)
907            if not template_path:
908                raise RuntimeError("config template not found for type: {}".format(file_type))
909
910        # deduce filename if not specified
911        if os.path.isdir(output_path):
912            output_path = os.path.join(output_path, '{}.conf'.format(file_type))
913
914        # just copy file as-is unless it's mako
915        if not template_path.endswith('.mako'):
916            shutil.copy(template_path, output_path)
917            return output_path
918
919        # render mako template
920        context = {
921            'app_title': "Rattail",
922            'appdir': '/srv/envs/poser',
923            'db_url': 'postresql://user:pass@localhost/poser',
924            'timezone': 'America/Chicago',
925            'pyramid_egg': 'poser',
926            'os': os,
927            'beaker_secret': 'TODO_YOU_SHOULD_CHANGE_THIS',
928            'beaker_key': 'poser',
929            'pyramid_host': '0.0.0.0',
930            'pyramid_port': 9080,
931        }
932        context.update(kwargs)
933        self.render_mako_template(template_path, context,
934                                  output_path=output_path)
935        return output_path
936
937    def find_config_template(self, name):
938        template_paths = self.config.getlist('rattail.config', 'templates',
939                                             default=['rattail:data/config'])
940        for template_path in template_paths:
941
942            # prefer mako templates
943            path = resource_path('{}/{}.conf.mako'.format(template_path.rstrip('/'),
944                                                          name))
945            if os.path.exists(path):
946                return path
947
948            # but plain config works too
949            path = resource_path('{}/{}.conf'.format(template_path.rstrip('/'),
950                                                     name))
951            if os.path.exists(path):
952                return path
953
954    def make_temp_dir(self, **kwargs):
955        """
956        Create a temporary directory.  This is mostly a convenience
957        wrapper around the built-in :func:`python:tempfile.mkdtemp()`.
958        However by default it will attempt to place the temp folder
959        underneath the configured "workdir", e.g.:
960
961        .. code-block:: ini
962
963           [rattail]
964           workdir = /srv/envs/poser/app/work
965        """
966        if 'dir' not in kwargs:
967            workdir = self.config.workdir(require=False)
968            if workdir:
969                tmpdir = os.path.join(workdir, 'tmp')
970                if not os.path.exists(tmpdir):
971                    os.makedirs(tmpdir)
972                kwargs['dir'] = tmpdir
973        return tempfile.mkdtemp(**kwargs)
974
975    def make_temp_file(self, **kwargs):
976        """
977        Reserve a temporary filename.  This is mostly a convenience
978        wrapper around the built-in :func:`python:tempfile.mkstemp()`.
979        However by default it will attempt to place the temp file
980        underneath the configured "workdir", e.g.:
981
982        .. code-block:: ini
983
984           [rattail]
985           workdir = /srv/envs/poser/app/work
986        """
987        if 'dir' not in kwargs:
988            workdir = self.config.workdir(require=False)
989            if workdir:
990                tmpdir = os.path.join(workdir, 'tmp')
991                if not os.path.exists(tmpdir):
992                    os.makedirs(tmpdir)
993                kwargs['dir'] = tmpdir
994        return temp_path(**kwargs)
995
996    def make_uuid(self):
997        """
998        Generate a new UUID value.
999
1000        :returns: UUID value as 32-character string.
1001        """
1002        return get_uuid()
1003
1004    def normalize_phone_number(self, number, **kwargs):
1005        """
1006        Normalize the given phone number, to a "common" format that
1007        can be more easily worked with for sync logic etc.  In
1008        practice this usually just means stripping all non-digit
1009        characters from the string.  The idea is that phone number
1010        data from any system can be "normalized" and thereby compared
1011        directly to see if they differ etc.
1012
1013        Default logic will invoke
1014        :func:`rattail.db.util.normalize_phone_number()`.
1015
1016        :param number: Raw phone number string e.g. as found in some
1017           data source.
1018
1019        :returns: Normalized string.
1020        """
1021        from rattail.db.util import normalize_phone_number
1022
1023        return normalize_phone_number(number)
1024
1025    def phone_number_is_invalid(self, number):
1026        """
1027        This method should validate the given phone number string, and
1028        if the number is *not* considered valid, this method should
1029        return the reason.
1030
1031        :param number: Raw phone number string e.g. as found in some
1032           data source.
1033
1034        :returns: String describing reason the number is invalid, or
1035           ``None`` if the number is deemed valid.
1036        """
1037        # strip non-numeric chars, and make sure we have 10 left
1038        normal = self.normalize_phone_number(number)
1039        if len(normal) != 10:
1040            return "Phone number must have 10 digits"
1041
1042    def format_phone_number(self, number):
1043        """
1044        Returns a "properly formatted" string based on the given phone
1045        number.
1046
1047        Default logic invokes
1048        :func:`rattail.db.util.format_phone_number()`.
1049
1050        :param number: Raw phone number string e.g. as found in some
1051           data source.
1052
1053        :returns: Formatted phone number string.
1054        """
1055        from rattail.db.util import format_phone_number
1056
1057        return format_phone_number(number)
1058
1059    def make_gpc(self, value, **kwargs):
1060        """
1061        Make and return a :class:`~rattail.gpc.GPC` instance from the
1062        given value.
1063
1064        Default logic will invoke
1065        :meth:`~rattail.products.ProductsHandler.make_gpc()` of the
1066        products handler; see also :meth:`get_products_handler()`.
1067        """
1068        products_handler = self.get_products_handler()
1069        return products_handler.make_gpc(value, **kwargs)
1070
1071    def render_gpc(self, value, **kwargs):
1072        """
1073        Returns a human-friendly display string for the given GPC
1074        value.
1075
1076        :param value: A :class:`~rattail.gpc.GPC` instance.
1077
1078        :returns: Display string for the GPC, or ``None`` if the value
1079           provided is not a GPC.
1080        """
1081        if value:
1082            return value.pretty()
1083
1084    def render_upc(self, value, **kwargs): # pragma: no cover
1085        warnings.warn("method is deprecated, please use "
1086                      "render_gpc() method instead",
1087                      DeprecationWarning)
1088        return self.render_gpc(value, **kwargs)
1089
1090    def render_currency(self, value, scale=2, **kwargs):
1091        """
1092        Must return a human-friendly display string for the given
1093        currency value, e.g. ``Decimal('4.20')`` becomes ``"$4.20"``.
1094
1095        :param value: Either a :class:`python:decimal.Decimal` or
1096           :class:`python:float` value.
1097
1098        :param scale: Number of decimal digits to be displayed.
1099
1100        :returns: Display string for the value.
1101        """
1102        if value is None:
1103            return ''
1104        if value < 0:
1105            fmt = "(${{:0,.{}f}})".format(scale)
1106            return fmt.format(0 - value)
1107        fmt = "${{:0,.{}f}}".format(scale)
1108        return fmt.format(value)
1109
1110    def render_quantity(self, value, **kwargs):
1111        """
1112        Return a human-friendly display string for the given quantity
1113        value, e.g. ``1.000`` becomes ``"1"``.
1114
1115        :param value: The quantity to be rendered.
1116
1117        :returns: Display string for the quantity.
1118        """
1119        return pretty_quantity(value, **kwargs)
1120
1121    def render_cases_units(self, cases, units):
1122        """
1123        Render a human-friendly string showing the given number of
1124        cases and/or units.  For instance::
1125
1126           >>> app.render_cases_units(1, None)
1127           '1 case'
1128
1129           >>> app.render_cases_units(None, 1)
1130           '1 unit'
1131
1132           >>> app.render_cases_units(3, 2)
1133           '3 cases + 2 units'
1134
1135        :param cases: Number of cases (can be zero or ``None``).
1136
1137        :param units: Number of units (can be zero or ``None``).
1138
1139        :returns: Display string for the given values.
1140        """
1141        if cases is not None:
1142            label = "case" if abs(cases) == 1 else "cases"
1143            cases = "{} {}".format(self.render_quantity(cases), label)
1144
1145        if units is not None:
1146            label = "unit" if abs(units) == 1 else "units"
1147            units = "{} {}".format(self.render_quantity(units), label)
1148
1149        if cases and units:
1150            return "{} + {}".format(cases, units)
1151
1152        return cases or units
1153
1154    def render_date(self, value, **kwargs):
1155        """
1156        Return a human-friendly display string for the given date.
1157
1158        :param value: A :class:`python:datetime.date` instance.
1159
1160        :returns: Display string for the date.
1161        """
1162        if value is not None:
1163            return value.strftime('%Y-%m-%d')
1164
1165    def render_datetime(self, value, **kwargs):
1166        """
1167        Return a human-friendly display string for the given datetime.
1168        """
1169        if value is not None:
1170            return value.strftime('%Y-%m-%d %I:%M:%S %p')
1171
1172    def render_duration(self, seconds=None, **kwargs):
1173        if seconds is None:
1174            return ""
1175        return pretty_hours(datetime.timedelta(seconds=seconds))
1176
1177    def render_percent(self, value, places=2, **kwargs):
1178        """
1179        Render a human-friendly display string for the given
1180        percentage value.
1181
1182        :param value: Should be a decimal representation of the
1183           percentage, e.g. ``0.80`` would indicate 80%.
1184
1185        :param places: Number of decimal places to display in the
1186           rendered string.
1187        """
1188        fmt = '{{:0.{}f}} %'.format(places)
1189        return fmt.format(value * 100)
1190
1191    def send_email(self, key, data={}, **kwargs):
1192        """
1193        Send an email message of the given type.
1194
1195        See :func:`rattail.mail.send_email()` for more info.
1196        """
1197        send_email(self.config, key, data, **kwargs)
1198
1199
1200class GenericHandler(object):
1201    """
1202    Base class for misc. "generic" feature handlers.
1203
1204    Most handlers which exist for sake of business logic, should inherit from
1205    this.
1206    """
1207
1208    def __init__(self, config, **kwargs):
1209        self.config = config
1210        self.enum = self.config.get_enum()
1211        self.model = self.config.get_model()
1212        self.app = self.config.get_app()
1213
1214    def progress_loop(self, *args, **kwargs): # pragma: no cover
1215        warnings.warn("method is deprecated, please use "
1216                      "AppHandler.progress_loop() method instead",
1217                      DeprecationWarning)
1218        return self.app.progress_loop(*args, **kwargs)
1219
1220    def get_session(self, obj): # pragma: no cover
1221        warnings.warn("method is deprecated, please use "
1222                      "AppHandler.get_session() method instead",
1223                      DeprecationWarning)
1224        return self.app.get_session(obj)
1225
1226    def make_session(self): # pragma: no cover
1227        warnings.warn("method is deprecated, please use "
1228                      "AppHandler.make_session() method instead",
1229                      DeprecationWarning)
1230        return self.app.make_session()
1231
1232    def cache_model(self, session, model, **kwargs): # pragma: no cover
1233        warnings.warn("method is deprecated, please use "
1234                      "AppHandler.cache_model() method instead",
1235                      DeprecationWarning)
1236        return self.app.cache_model(session, model, **kwargs)
1237
1238
1239def make_app(config, **kwargs): # pragma: no cover
1240    warnings.warn("function is deprecated, please use "
1241                  "RattailConfig.get_app() method instead",
1242                  DeprecationWarning)
1243    return config.get_app()
Note: See TracBrowser for help on using the repository browser.