source: rattail/rattail/app.py @ f458072

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

Add basic autocompleter for subdepartments

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