Esempio n. 1
0
    def __init__(self, site):
        # logger.info("20140227 Kernel.__init__() a")

        # from importlib import import_module
        # # For every plugin, Lino checks whether the package contains a
        # # module named `ui` and, if yes, imports this module. The
        # # benefit of this is that all "Lino extensions" to the models
        # # can be moved out of :xfile:`models.py` into a separate file
        # # :xfile:`ui.py`.
        # # print '\n'.join([p.app_name for p in self.installed_plugins])
        # for p in site.installed_plugins:
        #     # fn = dirname(inspect.getfile(p.app_module))
        #     # fn = join(fn, 'ui.py')
        #     try:
        #         x = p.app_name + '.ui'
        #         import_module(x)
        #         logger.info("20150416 imported %s", x)
        #     except Exception as e:
        #     # except ImportError as e:
        #         if str(e) != "No module named ui":
        #             logger.warning("Failed to import %s : %s", x, e)
        #             # raise Exception("Failed to import %s : %s" % (x, e))

        self.pending_threads = {}
        self.site = site
        self.GFK_LIST = []
        self.kernel_startup(site)
        self.code_mtime = codetime()
        # We set `code_mtime` only after kernel_startup() because
        # codetime watches only those modules which are already
        # imported.

        self.memo_parser = Parser()

        def url2html(parser, s):
            url_text = s.split(None, 1)
            if len(url_text) == 1:
                url = text = url_text[0]
            else:
                url, text = url_text
            return '<a href="%s">%s</a>' % (url, text)

        self.memo_parser.register_command('url', url2html)

        if site.build_js_cache_on_startup is None:
            site.build_js_cache_on_startup = not (
                settings.DEBUG or is_devserver())

        # web.site_setup(site)

        for a in actors.actors_list:
            if a.get_welcome_messages is not None:
                # site._welcome_actors.append(a)
                site.add_welcome_handler(a.get_welcome_messages)

        pre_ui_build.send(self)

        self.html_renderer = HtmlRenderer(self)
        self.text_renderer = TextRenderer(self)
        self.reserved_names = [getattr(constants, n)
                               for n in constants.URL_PARAMS]

        names = set()
        for n in self.reserved_names:
            if n in names:
                raise Exception("Duplicate reserved name %r" % n)
            names.add(n)

        for p in site.installed_plugins:
            p.on_ui_init(self)

        ui = None
        if self.site.default_ui is not None:
            ui = self.site.plugins.resolve(self.site.default_ui)
            if ui is None:
                raise Exception(
                    "Invalid value %r for `default_ui` (must be one of %s)"
                    % (self.site.default_ui, list(self.site.plugins.keys())))
            ui.url_prefix = None
        else:
            for p in self.site.installed_plugins:
                if p.ui_handle_attr_name is not None:
                    ui = p
                    break
            # if ui is None:
            #     raise Exception("No user interface in {0}".format(
            #         [u.app_name for u in self.site.installed_plugins]))
        if ui is not None:
            self.default_renderer = ui.renderer
            self.default_ui = ui

        post_ui_build.send(self)

        if self.default_ui is not None:

            # trigger creation of params_layout.params_store
            for res in actors.actors_list:
                for ba in res.get_actions():
                    if ba.action.params_layout is not None:
                        ba.action.params_layout.get_layout_handle(
                            self.default_ui)
Esempio n. 2
0
from lino.api import dd
from lino.core import constants
from lino.core import auth
from lino.core.requests import BaseRequest
from lino.core.tablerequest import TableRequest
from lino.core.views import action_request
from lino.core.utils import navinfo
from lino.utils.xmlgen.html import E
from lino.utils.xmlgen import html as xghtml

PLAIN_PAGE_LENGTH = 15

MENUS = dict()

from lino.utils.memo import Parser
MEMO_PARSER = Parser()


def url2html(s):
    url, text = s.split(None, 1)
    if not text:
        text = url
    return '<a href="%s">%s</a>' % (url, text)

MEMO_PARSER.register_command('url', url2html)


def http_response(ar, tplname, context):
    "Deserves a docstring"
    u = ar.get_user()
    lang = get_language()
Esempio n. 3
0
class Kernel(object):
    """This is the class of the object stored in :attr:`Site.kernel
<lino.core.site.Site.kernel>`.

    """
    default_ui = None

    # _singleton_instance = None

    # @classmethod
    # def instance(cls, site):
    #     if cls._singleton_instance is None:
    #         cls._singleton_instance = cls(site)
    #     elif cls._singleton_instance.site is not site:
    #         site.logger().info("Overriding SITE instance")
    #         cls._singleton_instance.site = site
    #     return cls._singleton_instance

    def __init__(self, site):
        # logger.info("20140227 Kernel.__init__() a")

        # from importlib import import_module
        # # For every plugin, Lino checks whether the package contains a
        # # module named `ui` and, if yes, imports this module. The
        # # benefit of this is that all "Lino extensions" to the models
        # # can be moved out of :xfile:`models.py` into a separate file
        # # :xfile:`ui.py`.
        # # print '\n'.join([p.app_name for p in self.installed_plugins])
        # for p in site.installed_plugins:
        #     # fn = dirname(inspect.getfile(p.app_module))
        #     # fn = join(fn, 'ui.py')
        #     try:
        #         x = p.app_name + '.ui'
        #         import_module(x)
        #         logger.info("20150416 imported %s", x)
        #     except Exception as e:
        #     # except ImportError as e:
        #         if str(e) != "No module named ui":
        #             logger.warning("Failed to import %s : %s", x, e)
        #             # raise Exception("Failed to import %s : %s" % (x, e))

        self.pending_threads = {}
        self.site = site
        self.GFK_LIST = []
        self.kernel_startup(site)
        self.code_mtime = codetime()
        # We set `code_mtime` only after kernel_startup() because
        # codetime watches only those modules which are already
        # imported.

        self.memo_parser = Parser()

        def url2html(parser, s):
            url_text = s.split(None, 1)
            if len(url_text) == 1:
                url = text = url_text[0]
            else:
                url, text = url_text
            return '<a href="%s">%s</a>' % (url, text)

        self.memo_parser.register_command('url', url2html)

        if site.build_js_cache_on_startup is None:
            site.build_js_cache_on_startup = not (
                settings.DEBUG or is_devserver())

        # web.site_setup(site)

        for a in actors.actors_list:
            if a.get_welcome_messages is not None:
                # site._welcome_actors.append(a)
                site.add_welcome_handler(a.get_welcome_messages)

        pre_ui_build.send(self)

        self.html_renderer = HtmlRenderer(self)
        self.text_renderer = TextRenderer(self)
        self.reserved_names = [getattr(constants, n)
                               for n in constants.URL_PARAMS]

        names = set()
        for n in self.reserved_names:
            if n in names:
                raise Exception("Duplicate reserved name %r" % n)
            names.add(n)

        for p in site.installed_plugins:
            p.on_ui_init(self)

        ui = None
        if self.site.default_ui is not None:
            ui = self.site.plugins.resolve(self.site.default_ui)
            if ui is None:
                raise Exception(
                    "Invalid value %r for `default_ui` (must be one of %s)"
                    % (self.site.default_ui, list(self.site.plugins.keys())))
            ui.url_prefix = None
        else:
            for p in self.site.installed_plugins:
                if p.ui_handle_attr_name is not None:
                    ui = p
                    break
            # if ui is None:
            #     raise Exception("No user interface in {0}".format(
            #         [u.app_name for u in self.site.installed_plugins]))
        if ui is not None:
            self.default_renderer = ui.renderer
            self.default_ui = ui

        post_ui_build.send(self)

        if self.default_ui is not None:

            # trigger creation of params_layout.params_store
            for res in actors.actors_list:
                for ba in res.get_actions():
                    if ba.action.params_layout is not None:
                        ba.action.params_layout.get_layout_handle(
                            self.default_ui)
        # logger.info("20140227 Kernel.__init__() done")

    def kernel_startup(kernel, self):
        """This is a part of a Lino site startup.  The Django Model
        definitions are done, now Lino analyzes them and does certain
        actions:

        - Verify that there are no more pending injects Install a
          :class:`DisableDeleteHandler
          <lino.core.ddh.DisableDeleteHandler>` for each Model into
          `_lino_ddh`.

        - Install :class:`lino.core.model.Model` attributes and
          methods into Models that don't inherit from it.

        """
        if len(sys.argv) == 0:
            process_name = 'WSGI'
        else:
            process_name = ' '.join(sys.argv)

        logger.info("Started %s (using %s) --> PID %s",
                    process_name, settings.SETTINGS_MODULE, os.getpid())
        # puts(self.welcome_text())

        def goodbye():
            logger.info("Done %s (PID %s)", process_name, os.getpid())
        atexit.register(goodbye)

        models_list = get_models(include_auto_created=True)
        # this also triggers django.db.models.loading.cache._populate()

        self.setup_model_spec(self, 'user_model')
        self.setup_model_spec(self, 'project_model')

        for app_name_model, p in list(self.override_modlib_models.items()):
            # app_name_model is the full installed app module name +
            # the model name. It certainly contains at least one dot.
            m = '.'.join(app_name_model.split('.')[-2:])
            resolve_model(
                m,
                strict="%s plugin tries to extend unresolved model '%%s'" %
                p.__class__.__module__)

        for model in models_list:
            #~ print 20130216, model
            #~ fix_field_cache(model)

            # if hasattr(model, '_lino_ddh'):
            if '_lino_ddh' in model.__dict__:
                raise Exception("20150831 %s", model)
            model._lino_ddh = DisableDeleteHandler(model)

            Model.django2lino(model)

            if isinstance(model.hidden_columns, basestring):
                model.hidden_columns = frozenset(
                    fields.fields_list(model, model.hidden_columns))

            if isinstance(model.active_fields, basestring):
                model.active_fields = frozenset(
                    fields.fields_list(model, model.active_fields))

            if isinstance(model.allow_cascaded_delete, basestring):
                model.allow_cascaded_delete = frozenset(
                    fields.fields_list(model, model.allow_cascaded_delete))

            qsf = model.quick_search_fields
            # Attention when inheriting this from from parent model.
            # qsf = model.__dict__.get('quick_search_fields', None)
            if qsf is None:
                fields_list = []
                for field in model._meta.fields:
                    if isinstance(field, models.CharField):
                        fields_list.append(field.name)
                model.quick_search_fields = frozenset(fields_list)
            elif isinstance(qsf, frozenset):
                pass
            elif isinstance(qsf, basestring):
                model.quick_search_fields = frozenset(
                    fields.fields_list(model, model.quick_search_fields))
            else:
                raise ChangedAPI(
                    "{0}.quick_search_fields must be None or a string "
                    "of space-separated field names (not {1})".format(
                        model, qsf))

            if model._meta.abstract:
                raise Exception("Tiens?")

            # self.modules.define(model._meta.app_label, model.__name__, model)

            for f in model._meta.virtual_fields:
                if isinstance(f, GenericForeignKey):
                    kernel.GFK_LIST.append(f)

        # vip_classes = (layouts.BaseLayout, fields.Dummy)
        # for a in models.get_apps():
        #     app_label = a.__name__.split('.')[-2]

        #     for k, v in a.__dict__.items():
        #         if isinstance(v, type) and issubclass(v, vip_classes):
        #             self.modules.define(app_label, k, v)

        #         if k.startswith('setup_'):
        #             self.modules.define(app_label, k, v)

        if self.user_profiles_module:
            from importlib import import_module
            import_module(self.user_profiles_module)
        
        self.setup_choicelists()
        self.setup_workflows()

        for model in models_list:
            if model._meta.auto_created:
                continue  # automatic intermediate models created by
                          # ManyToManyField should not disable delete
            # for f, m in model._meta.get_fields_with_model():
            for f in model._meta.get_fields():
                m = f.model

                # Refuse nullable CharFields, but don't trigger on
                # NullableCharField (which is a subclass of CharField).

                if f.__class__ is models.CharField and f.null:
                    msg = "Nullable CharField %s in %s" % (f.name, model)
                    raise Exception(msg)
                elif isinstance(f, models.ForeignKey):
                    if isinstance(f.rel.model, basestring):
                        raise Exception("Could not resolve target %r of "
                                        "ForeignKey '%s' in %s "
                                        "(models are %s)" %
                                        (f.rel.model, f.name, model, models_list))
                    set_default_verbose_name(f)

                    """
                    If JobProvider is an MTI child of Company,
                    then mti.delete_child(JobProvider) must not fail on a
                    JobProvider being referred only by objects that can refer
                    to a Company as well.
                    """
                    if not hasattr(f.rel.model, '_lino_ddh'):
                        raise Exception("20150824")
                    # f.rel.model._lino_ddh.add_fk(f.model, f)
                    # m = f.model._meta.concrete_model
                    # f.rel.model._lino_ddh.add_fk(m, f)
                    f.rel.model._lino_ddh.add_fk(m or model, f)

        kernel.protect_foreignkeys(models_list)

        for p in self.installed_plugins:
            if isinstance(p, Plugin):
                p.before_analyze()

        # logger.info("20150429 Gonna send pre_analyze signal")
        pre_analyze.send(self, models_list=models_list)
        # logger.info("20150429 pre_analyze signal done")
        # MergeActions are defined in pre_analyze.
        # And MergeAction needs the info in _lino_ddh to correctly find
        # keep_volatiles

        self.setup_actions()

        for model in models_list:

            """Virtual fields declared on the model must have been attached
            before calling Model.site_setup(), e.g. because
            pcsw.Person.site_setup() declares `is_client` as imported
            field.

            """

            model.on_analyze(self)

            for k, v in class_dict_items(model):
                if isinstance(v, fields.VirtualField):
                    v.attach_to_model(model, k)

        #~ logger.info("20130817 attached model vfs")

        actors.discover()

        logger.debug("actors.initialize()")
        for a in actors.actors_list:
            a.class_init()

        dbtables.discover()
        #~ choosers.discover()
        actions.discover_choosers()

        for a in actors.actors_list:
            a.on_analyze(self)

        post_analyze.send(self, models_list=models_list)

        if False:
            logger.info("Languages: %s. %d apps, %d models, %s actors.",
                        ', '.join([li.django_code for li in self.languages]),
                        len(self.modules),
                        len(models_list),
                        len(actors.actors_list))

        #~ logger.info(settings.INSTALLED_APPS)

        self.setup_layouts()

        self.on_each_app('site_setup')  # deprecated

        # Actor.after_site_setup() is called after the apps'
        # site_setup().  Example: pcsw.site_setup() adds a detail to
        # properties.Properties, the base class for
        # properties.PropsByGroup.  The latter would not install a
        # `detail_action` during her after_site_setup() and also would
        # never get it later.

        for a in actors.actors_list:
            a.after_site_setup(self)

        #~ self.on_site_startup()

        self.resolve_virtual_fields()

        #~ logger.info("20130827 startup_site done")

    def protect_foreignkeys(self, models_list):
        """Change `on_delete` from CASCADE (Django's default value) to PROTECT
        for foreignkeys that need to be protected.

        Basically we protect all FK fields that are not listed in
        their model's :attr:`allow_cascaded_delete
        <lino.core.model.Model.allow_cascaded_delete>`. With one
        exception: pointers to the MTI parent of a :class:`Polymorphic
        <lino.mixins.polymorphic.Polymorphic>` must not
        become protected (because Lino handles it automatically, see
        :meth:`lino.mixins.polymorphic.Polymorphic.disable_delete`).

        """

        from lino.mixins.polymorphic import Polymorphic

        def must_protect(m, fk, model):
            # Whether the given foreign key fk
            if fk.name in m.allow_cascaded_delete:
                return False
            if m is model:  # FK to self
                return True
            # if issubclass(m, model) or issubclass(model, m):
            if issubclass(m, model):
                if issubclass(m, Polymorphic):
                    # they have an MTI relation
                    return False
            return True

        for model in models_list:
            for m, fk in model._lino_ddh.fklist:
                assert fk.rel.model is model
                if fk.rel.on_delete == models.CASCADE:
                    if must_protect(m, fk, model):
                        msg = (
                            "Setting {0}.{1}.on_delete to PROTECT because "
                            "field is not specified in "
                            "allow_cascaded_delete.").format(fmn(m), fk.name)
                        logger.debug(msg)
                        fk.rel.on_delete = models.PROTECT
                else:
                    if fk.name in m.allow_cascaded_delete:
                        msg = ("{0}.{1} specified in allow_cascaded_delete "
                               "but on_delete is not CASCADE").format(
                            fmn(m), fk.name)
                        raise Exception(msg)
    
                    if fk.rel.on_delete == models.SET_NULL:
                        if not fk.null:
                            msg = ("{0}.{1} has on_delete SET_NULL but "
                                   "is not nullable ")
                            msg = msg.format(fmn(m), fk.name, fk.rel.model)
                            raise Exception(msg)

                    else:
                        msg = ("{0}.{1} has custom on_delete").format(
                            fmn(m), fk.name, fk.rel.on_delete)
                        logger.debug(msg)
                
    def get_generic_related(self, obj):
        """Yield a series of `(gfk, fk_field, queryset)` tuples which together
         will return all database objects for which the given
         GenericForeignKey gfk points to the object `obj`. See also
         :doc:`/dev/gfks`.

        """
        if len(self.GFK_LIST) == 0:
            return  # e.g. if contenttypes is not installed

        from django.contrib.contenttypes.models import ContentType

        if not isinstance(obj._meta.pk, GFK_TARGETS):
            # raise Exception("20150330 %s", obj._meta.pk)
            return  # e.g. Country.iso_code is a CharField, cannot
                    # point to a country using GFK
        obj_ct = ContentType.objects.get_for_model(obj.__class__)
        # logger.info("20150330 ok %s", obj_ct)
        for gfk in self.GFK_LIST:
            fk_field = gfk.model._meta.get_field(gfk.fk_field)
            # fk_field, remote_model, direct, m2m = \
            #     gfk.model._meta.get_field_by_name(gfk.fk_field)
            kw = dict()
            kw[gfk.fk_field] = obj.pk
            kw[gfk.ct_field] = obj_ct
            ct = ContentType.objects.get_for_model(gfk.model)
            yield gfk, fk_field, ct.get_all_objects_for_this_type(**kw)

    def get_broken_generic_related(self, model):
        """Yield all database objects of this model which have some broken
        GFK field.
    
        This is a slow query which does an additional database request
        for each row. (Is there a possibility to do this in a single
        SQL query?)
    
        Each yeld object has two special attributes:
    
        - `_message` : a textual description of the problem
        - `_todo` : 'delete', 'clear' or 'manual'

        Note: the "clear" action should not run automatically, at
        least not for :mod:`lino.modlib.changes`.

        See also :ref:`lino.tutorial.watch`.

        """
        gfks = [f for f in self.GFK_LIST if f.model is model]
        if len(gfks):
            for gfk in gfks:
                fk_field = gfk.model._meta.get_field(gfk.fk_field)
                # fk_field, remote_model, direct, m2m = \
                #     gfk.model._meta.get_field_by_name(gfk.fk_field)
                kw = {gfk.ct_field+'__isnull': False}
                qs = model.objects.filter(**kw)
                for obj in qs:
                    fk = getattr(obj, gfk.fk_field)
                    ct = getattr(obj, gfk.ct_field)
                    pointed_model = ct.model_class()
                    # pointed_model = ContentType.objects.get_for_id(ct)
                    try:
                        pointed_model.objects.get(pk=fk)
                    except pointed_model.DoesNotExist:
                        msg = "Invalid primary key {1} for {2} in `{0}`"
                        obj._message = msg.format(
                            gfk.fk_field, fk, fmn(pointed_model))
                        if gfk.name in model.allow_cascaded_delete:
                            obj._todo = 'delete'
                        elif fk_field.null:
                            obj._todo = 'clear'
                        else:
                            obj._todo = 'manual'
                        yield obj

    def abandon_response(self):
        return self.success(_("User abandoned"))

    def field2elem(self, lui, field, **kw):
        pass

    def run_callback(self, request, thread_id, button_id):
        """Continue the action which was started in a previous request and
        which asked for user interaction via a :class:`Callback`.

        This is called from `lino.core.views.Callbacks`.

        """
        # logger.info("20131212 get_callback %s %s", thread_id, button_id)

        # 20140304 Also set a renderer so that callbacks can use it
        # (feature needed by beid.FindByBeIdAction).

        thread_id = int(thread_id)
        cb = self.pending_threads.pop(thread_id, None)
        if cb is None:
            ar = ActorRequest(request, renderer=self.default_renderer)
            logger.debug("No callback %r in %r" % (
                thread_id, list(self.pending_threads.keys())))
            ar.error("Unknown callback %r" % thread_id)
            return self.render_action_response(ar)

        # e.g. SubmitInsertClient must set `data_record` in the
        # callback request ("ar2"), not the original request ("ar"),
        # i.e. the methods to create an instance and to fill
        # `data_record` must run on the callback request.  So the
        # callback request must be a clone of the original request.
        # New since 20140421
        ar = cb.ar.actor.request_from(cb.ar)
        for k in CLONEABLE_ATTRS:
            setattr(ar, k, getattr(cb.ar, k))

        for c in cb.choices:
            if c.name == button_id:
                a = ar.bound_action.action
                if self.site.log_each_action_request and not a.readonly:
                    logger.info("run_callback {0} {1} {2}".format(
                        thread_id, cb.message, c.name))
                c.func(ar)
                return self.render_action_response(ar)

        ar.error("Invalid button %r for callback" % (button_id, thread_id))
        return self.render_action_response(ar)

    def add_callback(self, ar, *msgs):
        """Returns an "action callback" which will initiate a dialog thread
        by asking a question to the user and suspending execution until
        the user's answer arrives in a next HTTP request.

        Calling this from an Action's :meth:`run_from_ui
        <lino.core.actions.Action.run_from_ui>` method will interrupt
        the execution, send the specified message back to the user,
        adding the executables `yes` and optionally `no` to a queue of
        pending "dialog threads".

        The client will display the prompt and will continue this
        thread by requesting
        :class:`lino.modlib.extjs3.views.Callbacks`.

        """
        if len(msgs) > 1:
            msg = '\n'.join([force_text(s) for s in msgs])
        else:
            msg = msgs[0]

        return Callback(ar, msg)

    def set_callback(self, ar, cb):
        """
        """
        h = hash(cb)
        self.pending_threads[h] = cb
        # logger.info("20131212 Stored %r in %r" % (
        #     h, self.pending_threads))

        buttons = dict()
        for c in cb.choices:
            buttons[c.name] = c.label

        ar.success(
            cb.message, xcallback=dict(
                id=h,
                title=cb.title,
                buttons=buttons))

    def run_action(self, ar):
        """Run the action, catching some exceptions in order to report them
        in a user-friendly way.

        """
        
        a = ar.bound_action.action
        if self.site.log_each_action_request and not a.readonly:
            flds = []
            A = flds.append
            a = ar.bound_action.action
            # A(a.__class__.__module__+'.'+a.__class__.__name__)
            A(ar.get_user().username)
            A(ar.bound_action.full_name())
            A(obj2str(ar.master_instance))
            A(obj2str(ar.selected_rows))
            # A(format_request(ar.request))
            logger.info("run_action {0}".format(' '.join(flds)))
            # logger.info("run_action {0}".format(ar))
        try:
            a.run_from_ui(ar)
            if a.parameters and not a.no_params_window:
                ar.set_response(close_window=True)
        except exceptions.ValidationError as e:
            # logger.info("20150127 run_action %r", e)
            ar.error(ar.ah.actor.error2str(e), alert=True)
        except Warning as e:
            ar.error(str(e), alert=True)

        return self.render_action_response(ar)

    def setup_handle(self, h, ar):
        """
        ar is usually None, except for actors with dynamic handle
        """
        #~ logger.info('20121010 ExtUI.setup_handle() %s',h.actor)

        if h.actor.is_abstract():
            return

        if isinstance(h, tables.TableHandle):
            he = set(h.actor.hidden_columns | h.actor.hidden_elements)
            ll = layouts.ColumnsLayout(
                h.actor.get_column_names(ar),
                h.actor, hidden_elements=he)
            h.list_layout = ll.get_layout_handle(self.default_ui)
        else:
            h.list_layout = None

        if h.actor.params_layout:
            h.params_layout_handle = h.actor.make_params_layout_handle(self)

        h.store = Store(h)

    def render_action_response(self, ar):
        """Builds a JSON response from response information stored in given
        ActionRequest.

        """
        return views.json_response(ar.response, ar.content_type)

    def row_action_button(self, *args, **kw):
        """
        See :meth:`ExtRenderer.row_action_button`
        """
        return self.default_renderer.row_action_button(*args, **kw)

    _must_build = False

    def must_build_site_cache(self):
        self._must_build = True
        
    def make_cache_file(self, fn, write, force=False):
        """Make the specified cache file.  This is used internally at server
        startup.

        """
        # cachedir = self.site.cache_dir.child('static', 'cache', 'js')
        # if not exists(settings.STATIC_ROOT):
        #     logger.info("STATIC_ROOT does not exist: %s", settings.STATIC_ROOT)
        #     return 0
        fn = join(settings.MEDIA_ROOT, fn)
        # fn = join(settings.STATIC_ROOT, fn)
        # fn = join(self.site.cache_dir, fn)
        if not force and not self._must_build and exists(fn):
            mtime = os.stat(fn).st_mtime
            if mtime > self.code_mtime:
                logger.debug("%s (%s) is up to date.", fn, time.ctime(mtime))
                return 0

        logger.info("Building %s ...", fn)
        self.site.makedirs_if_missing(dirname(fn))
        f = codecs.open(fn, 'w', encoding='utf-8')
        try:
            write(f)
            f.close()
            return 1
        except Exception:
            f.close()
            if not self.site.keep_erroneous_cache_files:  #
                os.remove(fn)
            raise