maposmatic-dev
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Maposmatic-dev] [PATCH maposmatic] Improve the file cleanup mechanism


From: Maxime Petazzoni
Subject: [Maposmatic-dev] [PATCH maposmatic] Improve the file cleanup mechanism
Date: Thu, 4 Feb 2010 22:54:05 +0100

Previously, when the rendering directory was over the defined threshold,
files where removed progressively, oldest first, to make up some space.
No information was kept about jobs whose files were removed, making it
harder to keep track of valid jobs with files available.

This change introduces two new things:

  1. a new job status, 3, for jobs processed but now without files, or
     "obsolete" jobs.
  2. a new cleanup mechanism that considers jobs as the atomic unit of
     cleaning instead of files, as this would leave with jobs without
     all their renderings (which didn't make much sense).

The cleanup function underwent the following modifications:

  * files are now sorted by content modification time and not last
    metadata change (a simple chmod could mess up the order);
  * thumbnails are excluded from the list of considered files for
    removal (this is still is discussion, but for now let's keep them);
  * when a file needs to be removed, all files from its parent job are
    removed and the job's status is set to 3 (see
    MapRenderingJob#remove_all_files).
  * if no parent job can be found, it's an orphaned file and can be
    safely removed. Files starting with a '.' are of course preserved;
  * some logging improvements during the cleanup phase.

New 'job-done-obsolete' and 'job-error-obsolete' status icons are now
available, and the status icon filename is now inferred with a custom
template tag (this also led to some cleanup in extratags.py).

The file size of the renderings is also displayed next to each format in
the job information.

Signed-off-by: Maxime Petazzoni <address@hidden>
---
 scripts/maposmaticd                      |   74 ++++++++++++++++++----
 www/maposmatic/models.py                 |   98 ++++++++++++++++++++++++------
 www/maposmatic/templatetags/extratags.py |   45 ++++++++------
 www/media/job-done-obsolete.png          |  Bin 0 -> 7537 bytes
 www/media/job-error-obsolete.png         |  Bin 0 -> 5007 bytes
 www/media/style.css                      |    4 +
 www/templates/maposmatic/job-page.html   |    2 +-
 www/templates/maposmatic/job.html        |   27 +++-----
 8 files changed, 182 insertions(+), 68 deletions(-)
 create mode 100644 www/media/job-done-obsolete.png
 create mode 100644 www/media/job-error-obsolete.png

diff --git a/scripts/maposmaticd b/scripts/maposmaticd
index 4464913..8fcc56f 100755
--- a/scripts/maposmaticd
+++ b/scripts/maposmaticd
@@ -148,25 +148,71 @@ def render_job(job):
             job.end_rendering(resultmsg)
             return
 
-# This function checks that the total size of the files in
-# RENDERING_RESULT_PATH does not exceed 80% of
-# RENDERING_RESULT_MAX_SIZE_GB. If it does, the function removes as
-# many files as needed, oldest first
 def cleanup_files():
-    files = [ os.path.join(RENDERING_RESULT_PATH, f) for f in 
os.listdir(RENDERING_RESULT_PATH)]
-    files = [(f, os.stat(f).st_ctime, os.stat(f).st_size) for f in files]
+    """This cleanup function checks that the total size of the files in
+    RENDERING_RESULT_PATH does not exceed 80% of the defined threshold
+    RENDERING_RESULT_MAX_SIZE_GB. If it does, files are removed until the
+    constraint is met again, oldest first, and grouped by job."""
+
+    def get_formatted_value(v):
+        return '%.2f MiB' % (v/1024.0/1024.0)
+    def get_formatted_details(saved, size, threshold):
+        return 'saved %s, now %s/%s' % \
+                (get_formatted_value(saved),
+                 get_formatted_value(size),
+                 get_formatted_value(threshold))
+
+    files = [os.path.join(RENDERING_RESULT_PATH, f)
+                for f in os.listdir(RENDERING_RESULT_PATH)
+                if not f.startswith('.')]
+    files = map(lambda f: (f, os.stat(f).st_mtime, os.stat(f).st_size), files)
+
+    # Compute the total size occupied by the renderings, and the actual 80%
+    # threshold, in bytes
     size = reduce(lambda x, y: x + y[2], files, 0)
     threshold = 0.8 * RENDERING_RESULT_MAX_SIZE_GB * 1024 * 1024 * 1024
+
+    # Stop here if we are below the threshold
     if size < threshold:
         return
-    files.sort(lambda x, y: cmp(x[1], y[1]))
-    for f in files:
-        os.remove(os.path.join(RENDERING_RESULT_PATH, f[0]))
-        size -= f[2]
-        LOG.debug("remove '%s', %f GB consumed over a %f GB threshold" % \
-                            (f[0], (size / 1024 / 1024 / 1024), (threshold / 
1024 / 1024 / 1024)))
-        if size < threshold:
-            break
+
+    LOG.info("%s consumed for a %s threshold. Cleaning..." %
+            (get_formatted_value(size), get_formatted_value(threshold)))
+
+    # Sort files by timestamp, oldest last, and start removing them by
+    # pop()-ing the list
+    files.sort(lambda x, y: cmp(y[1], x[1]))
+
+    while size > threshold:
+        if not len(files):
+            LOG.error("No files to remove and still above threshold! 
Something's wrong!")
+            return
+
+        # Get the next file to remove, and try to identify the job it comes
+        # from
+        f = files.pop()
+        name = os.path.basename(f[0])
+        job = MapRenderingJob.objects.get_by_filename(name)
+        if job:
+            removed, saved = job.remove_all_files()
+            size -= saved
+
+            # If files were removed, log it. If not, it only means only the
+            # thumbnail remained, and that's good.
+            if removed:
+                LOG.info("Removed %d files from job #%d (%s)." %
+                         (removed, job.id, get_formatted_details(saved, size, 
threshold)))
+
+
+        else:
+            # If we didn't find a parent job, it means this is an orphaned
+            # file, and we can safely remove it to get back some disk space.
+            os.remove(f[0])
+            saved = f[2]
+            size -= saved
+            LOG.info("Removed orphan file %s (%s)." %
+                     (name, get_formatted_details(saved, size, threshold)))
+
 
 if not os.path.isdir(RENDERING_RESULT_PATH):
     LOG.error("ERROR: please set RENDERING_RESULT_PATH ('%s') to an existing 
directory" % \
diff --git a/www/maposmatic/models.py b/www/maposmatic/models.py
index dc3b3ce..22ca137 100644
--- a/www/maposmatic/models.py
+++ b/www/maposmatic/models.py
@@ -42,12 +42,29 @@ class MapRenderingJobManager(models.Manager):
     # has its thumbnail present.
     def get_random_with_thumbnail(self):
         fifteen_days_before = datetime.now() - timedelta(15)
-        maps = 
MapRenderingJob.objects.filter(status=2).filter(submission_time__gte=fifteen_days_before).order_by('?')[0:10]
+        maps = (MapRenderingJob.objects.filter(status=2)
+            .filter(submission_time__gte=fifteen_days_before)
+            .order_by('?')[0:10])
         for m in maps:
             if m.get_thumbnail():
                 return m
         return None
 
+    def get_by_filename(self, name):
+        """Tries to find the parent MapRenderingJob of a given file from its
+        filename. Both the job ID found in the first part of the prefix and the
+        entire files_prefix is used to match a job."""
+
+        try:
+            jobid = int(name.split('_', 1)[0])
+            job = MapRenderingJob.objects.get(id=jobid)
+            if name.startswith(job.files_prefix()):
+                return job
+        except (ValueError, IndexError):
+            pass
+
+        return None
+
 SPACE_REDUCE = re.compile(r"\s+")
 NONASCII_REMOVE = re.compile(r"[^A-Za-z0-9]+")
 
@@ -57,6 +74,7 @@ class MapRenderingJob(models.Model):
         (0, 'Submitted'),
         (1, 'In progress'),
         (2, 'Done'),
+        (3, 'Done w/o files')
         )
 
     maptitle = models.CharField(max_length=256)
@@ -98,6 +116,7 @@ class MapRenderingJob(models.Model):
                              
self.startofrendering_time.strftime("%Y-%m-%d_%H-%M"),
                              self.maptitle_computized())
 
+
     def start_rendering(self):
         self.status = 1
         self.startofrendering_time = datetime.now()
@@ -109,20 +128,26 @@ class MapRenderingJob(models.Model):
         self.resultmsg = resultmsg
         self.save()
 
-    def is_waiting(self):
-        return self.status == 0
+    def rendering_time_gt_1min(self):
+        if self.needs_waiting():
+            return False
+
+        delta = self.endofrendering_time - self.startofrendering_time
+        return delta.seconds > 60
 
-    def is_rendering(self):
-        return self.status == 1
+    def __is_ok(self):              return self.resultmsg == 'ok'
 
-    def is_done(self):
-        return self.status == 2
+    def is_waiting(self):           return self.status == 0
+    def is_rendering(self):         return self.status == 1
+    def needs_waiting(self):        return self.status  < 2
 
-    def is_done_ok(self):
-        return self.is_done() and self.resultmsg == "ok"
+    def is_done(self):              return self.status == 2
+    def is_done_ok(self):           return self.is_done() and self.__is_ok()
+    def is_done_failed(self):       return self.is_done() and not 
self.__is_ok()
 
-    def is_done_failed(self):
-        return self.is_done() and self.resultmsg != "ok"
+    def is_obsolete(self):          return self.status == 3
+    def is_obsolete_ok(self):       return self.is_obsolete() and 
self.__is_ok()
+    def is_obsolete_failed(self):   return self.is_obsolete() and not 
self.__is_ok()
 
     def get_map_fileurl(self, format):
         return www.settings.RENDERING_RESULT_URL + "/" + self.files_prefix() + 
"." + format
@@ -137,23 +162,60 @@ class MapRenderingJob(models.Model):
         return os.path.join(www.settings.RENDERING_RESULT_PATH, 
self.files_prefix() + "_index." + format)
 
     def output_files(self):
+        """Returns a structured dictionary of the output files for this job.
+        The result contains two lists, 'maps' and 'indeces', listing the output
+        files. Each file is reported by a tuple (format, path, title, size)."""
+
         allfiles = {'maps': [], 'indeces': []}
 
         for format in www.settings.RENDERING_RESULT_FORMATS:
             # Map files (all formats but CSV)
-            if format != 'csv' and 
os.path.exists(self.get_map_filepath(format)):
-                allfiles['maps'].append((format, self.get_map_fileurl(format),
-                    _("%(title)s %(format)s Map") % {'title': self.maptitle, 
'format': format.upper()}))
+            map_path = self.get_map_filepath(format)
+            if format != 'csv' and os.path.exists(map_path):
+                allfiles['maps'].append((format, map_path,
+                    _("%(title)s %(format)s Map") % {'title': self.maptitle,
+                                                     'format': format.upper()},
+                    os.stat(map_path).st_size))
+
             # Index files
-            if os.path.exists(self.get_index_filepath(format)):
-                allfiles['indeces'].append((format, 
self.get_index_fileurl(format),
-                    _("%(title)s %(format)s Index") % {'title': self.maptitle, 
'format': format.upper()}))
+            index_path = self.get_index_filepath(format)
+            if os.path.exists(index_path):
+                allfiles['indeces'].append((format, index_path,
+                    _("%(title)s %(format)s Index") % {'title': self.maptitle,
+                                                       'format': 
format.upper()},
+                    os.stat(index_path).st_size))
 
         return allfiles
 
     def has_output_files(self):
+        """This function tells whether this job still has its output files
+        available on the rendering storage.
+
+        Their actual presence is checked if the job is considered done and not
+        yet obsolete."""
+
+        if self.is_done():
+            files = self.output_files()
+            return len(files['maps']) + len(files['indeces'])
+
+        return False
+
+    def remove_all_files(self):
+        """Removes all the output files from this job, and returns the space
+        saved in bytes (Note: the thumbnail is not removed)."""
+
         files = self.output_files()
-        return len(files['maps']) + len(files['indeces'])
+        saved = 0
+        removed = 0
+
+        for f in (files['maps'] + files['indeces']):
+            saved += f[3]
+            removed += 1
+            os.remove(f[1])
+
+        self.status = 3
+        self.save()
+        return removed, saved
 
     def get_thumbnail(self):
         thumbnail_file = os.path.join(www.settings.RENDERING_RESULT_PATH, 
self.files_prefix() + "_small.png")
diff --git a/www/maposmatic/templatetags/extratags.py 
b/www/maposmatic/templatetags/extratags.py
index 46de4d9..5f62d06 100644
--- a/www/maposmatic/templatetags/extratags.py
+++ b/www/maposmatic/templatetags/extratags.py
@@ -21,40 +21,49 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import datetime
+
 from django import template
-from django.utils.html import conditional_escape
 from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _
-import datetime
 
 register = template.Library()
 
 def job_status_to_str(value, arg, autoescape=None):
-
-    if autoescape:
-        esc = conditional_escape
-    else:
-        esc = lambda x: x
-
     if value == 0:
-        result = _("Waiting rendering")
+        return _("Waiting rendering")
     elif value == 1:
-        result = _("Rendering in progress")
+        return _("Rendering in progress")
     elif value == 2:
-        if arg == "ok":
-            result = _("Rendering successful")
+        if arg == 'ok':
+            return _("Rendering successful")
+        else:
+            return _("Rendering failed, please contact address@hidden")
+    elif value == 3:
+        if arg == 'ok':
+            return _("Rendering was successful, but the files are no longer "
+                     "available")
         else:
-            result = _("Rendering failed, please contact " \
-                       "address@hidden")
-    else:
-        result = ""
+            return _("Rendering failed, and the incomplete files were "
+                      "removed")
+
+    return ''
 
-    return result
+def job_status_to_icon_name(value, arg, autoescape=None):
+    if value == 0:          return 'job-in-queue'
+    if value == 1:          return 'job-in-progress'
+    if value == 2:
+        if arg == 'ok':     return 'job-done'
+        else:               return 'job-error'
+    if value == 3:
+        if arg == 'ok':     return 'job-done-obsolete'
+        else:               return 'job-error-obsolete'
 
-job_status_to_str.needs_autoescape = True
+    return 'job-error'
 
 def feedparsed(value):
     return datetime.datetime(*value[:6])
 
 register.filter('job_status_to_str', job_status_to_str)
+register.filter('job_status_to_icon_name', job_status_to_icon_name)
 register.filter('feedparsed', feedparsed)
diff --git a/www/media/job-done-obsolete.png b/www/media/job-done-obsolete.png
new file mode 100644
index 
0000000000000000000000000000000000000000..cbf2ec9697305c08755e710dbe714f5be012aedd
GIT binary patch
literal 7537
zcmW+*2Rzj8AOGIDID3yIJEM-QgviJy*<{ZYLL70{*}p>8SrIZ)$zCBGvdJc7MXpPB
zRvG`tpF8(*ue;address@hidden(1v>=<LDX8Bs)pdz?)-@)1%JP1Je2`2Fdst=
z6{zeB2NC>3>Y$^c3Z0$*<~0|mfOp8fH1GR>address@hidden|V$
z|4S+aacpa;DjWOHtOo~raZEI?KE+_>M+bhEj`XV}i|EujDa4VGGFY2CTv8FUv*F>%
address@hidden|7sV?Qcam*!GRIpdAE<e%aYtpx83`Cm+&n;cb)address@hidden>JXE0
z>Z1?UM|Fn^nubop8a<mP6vjj-+waPlSdWzxGmlyp){a96*|6K5x%ES{g!SG1gP9(e
zy;xr~8bwk8$4fx*P#V1og>aDvbOe2ZR3S+yANme;!Lo~RG-6af7Icv<jDmVWhqACP
zs1=T<hRVpcXrVsVA7l&?s=~nubUU2bP~yf$&S5sTVRC-nMibe<h`$N5D*g1J(%i0o
zcIn{XlTK!{y!Y=ZqN1X_8mzqJUU|uIK-lT&`(7MmFP7<OV#FmR!k3op;}Q}g2m#3O
zyr2(address@hidden@<c2s9e~;lh>>))Myh3SNPec4lT~cxL8#S{lpv_&DC9
zK7tM&ZNG9?jubLrx;`ybM5jhyU0p5nbb~|hm0T29!{R`u<f05szL2)address@hidden
zz{Pdg#GO}Ny-TW1JRs;&A$hhi%^z1^x(!cEM7?<<U|address@hidden@2f6{)zibhJ4t
z+DYOWS}xxB>IDcareby-JcLjv)qnumzW)BD-6L*fxURN5XZJ{fMrG((2m%iC%0?aY
zV&@address@hidden&S5sl_eK8H#A&an=0>4HM1cv9wU#oPt+?G7ZbyVg~76K
z2eTIx6!bVraDV<bRfuaRqVvnjX!_HyYZw_ZrlIchx(address@hidden|F>{<dVq{lF=kSO
zW8sG)MSr%JyAwjUZYtxm;}>9yif}s=3+2r0?31<9`h0N3NIGOVR*4&{ctX6##>SR-
zH<ION!+&address@hidden;u2)K>>-GZSW0tc6KFUHMv(^ukXZEJl(X3i;L6DeTCT4mB>8K
zJvlichSr~N%s1*qK}Z|PQ`69p8MN+Zd*{v_61He%eulQD&6d~#jfB3{NIGxwo(G!4
zB(address@hidden>2h(f*Io(SgG2302Jh_=Dv~qn|@DX!!b1|pZ^uv>rt9ERw
zY7niK;(kuxM%zrSTU!;XeMxLS_`n+d^ywWYwTozl#iHPX2b%Ev)GdEg4Jp$5qsq!e
zgSKOBBj-Fcz(&kB2X4nG9>a)ecE}%6{7EDRczjU}t|Ncw=4RO^HsAq0j-<2l8NBiF
address@hidden<vbtu>IxM<|00fBoAi;|G?7YYRKXcjx#J*!=kg((__j*_DX*K{D~EMez?^^
z3odjFCk*AL&o|zqi}WOghxHDjU?pbdE!s?-!4Sn|8nTV1&D$TWn^;(4FElqd`|l3k
zq>address@hidden&ireBnB4gWKcz)6lX9G{q2+Mo5*(9$9|e8=RsIUj$p5NbO*4l`hyx~v!y
zbSSBz<l;iWXcIXhkJg2R7)}h*#KeRS0c-s?w>dmERzUudJxI9jW<K?F4R_W!fj7+g
z{x|P_5%cN?)CgsK5}(FaiR}qRuM-0#DnV6!`nA%rIaUqpOrO1O4f&address@hidden
zKY1c9ARw^Scdgvc%S)fpf3FhJveSsRS!=e)${{5l(U&?JFg^ME=aH_iZr<zHu!)HY
z^G3Ard7q)>)?ef1mk<6OkVUkOw93KO*Vk{-tJsj)Synn+XxS_EY+LtR{?_2*|0C5*
zOhMr>GDtxZN4;address@hidden(address@hidden)z;&pjB?fD76fB&NC2lKF&mfKz&1Vlw;e*SmbHaoFn
zGrseFrG)ljUNv=f4!f&^paEq9cKGN0m#Mep%dbsL<CFR4kw2#kL&Y`!>t6o3zODw&
zqqdID$mnP|Kq!6&@|#%8<B+pUZFXLNjRf<+p1|;address@hidden|fCe}wio=B{bT
zPAeJD1c^xE2026`A(fs_KXn2=XlPzdZGUG}tZ!)<o1DymvTkGwQ-X--8Ir}W=g*&8
zgK6%)qZeZ(w36htR8=w1-ah2c))RcerTC(LA;address@hidden
zz_O{SsfC1vm94EW!^0r|oa-d|tW=~2C>+!!<;c>%3(f58?0B7>9CZBfwzso`JX=rv
zOihL!P<V%xvnppc=d4NAAd||94TXQt)KZR(jlF&address@hidden)
zD#`IaR2{>C8x1kWCMHYYJ7d3Ay9`~C<&address@hidden;v%<cnbott
zK5Z!}DLXWp$+X1xk%`H~;_&eByL-AMrll4ph{asUZhfY<u(Fc=nJV0~<w(Fk=O+KQ
zdXaoqjm<`2JYDKtM&address@hidden
address@hidden&-V_)67NN|H^FwlN&hVN{PK8fj^=q&address@hidden@wVs
zwy(address@hidden>VSE>PymCdp3?+epCRBRvYJJM`;>1EQNT~TXgDz)qcir
z1`%#;`OELfW(`JXc{Gwki(WDwr#tDb-&%em{>wjfgmm59uDvTOQ%A9Mv_+6nhi<z=
zY)@XQ-*Uz9XH39LHyiCcNac^WI;vK#<&I5GlGe{ss~{_<-OI0E{eSz@<5_?E-70H6
address@hidden&Rgw%Df>7=OOtl_VrVM<T*Z=qgdtwZ$6&kXAVj8_*!A#
zYydYW89i%mKqB>address@hidden)X>bKIiHL~o
z!m0P3ii(J+85q!mn!8KSo{?6pRNb)address@hidden;i~51A}CZOgMMg#A;0s##JCKB>Nk3u
zr|s=3?h7rK!B2E<=<MTqEt-baYqm#yU7cdeXVj2|g~g(GgV*VCfUce>=5D+GhccJw
zIP~{88As=m($Y?!A%&Qk8H<address@hidden>%BMQFy{nQf415lQvh^hO<<qnsk
zi_g$33~V*^^~()}oI6jRY==iam}~US!?i!C7HM66aVt$U1B&hRz6n`6J74)^b6w>%
zT~}AvR{gihQp*^w)2$dUxxu|++-V14x6PxT1a!&SX1Zf|3k(97K?YG#Q3buh8=AF<
z*3X5WoqVlwj^!X`oXG#EEh%rYkhpWHsj-o54DrFD_B|)w#I%GN940`oZ*E!VvTRv6
ztCe~e+EiPV`X6t1Kl!&_zZPOz$8+6c>gCIq+(<ZzCAKLqZ?XRkIXJItP+nsrI~aj0
z_)n6&AHf^j&&>HgY<#BBt*<f<Q)?rVhnte;q|FKW`v^e)WMYjApXEtQem<{o+*x;2
zMqVg{1#Jc4J7ZbzlDHwf6omWtmiIJ!os)4>*REczaO|d$l9pya{a|$<JlmIg?TUTN
zJPL+TVucP*LiDwP9`W(>ihJKekH<0m{mgjJjyPI$zqhYQ_SfXMZ*eX;0i-{E{D?+^
z4d_o3i4b~j$M(address@hidden(sFe3Fg(j3o%tZ}cJm$B%l-A)bC~Sr
z45R|wl76gIOWsHNV&~X*$~s0cH;5r1ARziU&OxS0SOv*bG3`EjGIdb^YdAvzIXgS&
z;address@hidden&
zwB$xHo#x6fdWV%j&0un6I_V5?<#;F&@UQ_Bph=c14!x_~sYls?o6W42^&Y838m>S{
z9&S8sjs5gVe(V(QP-zF2`0?LHlaF8DIHSV&zXvtvI%(UVqt{8o;U$#N?U(`aEv{m6
z$B3X(@?r6pZvF~4HZTY~U#4`Hh#R_4e$%_5;_<9B+SfOefxWiD=(epEuBQCTL#hrt
zcXz5Kufsc%0&nRda3+O{Bw&~qL#RcI&E0<|fJDjU<m9~J<HN&P4bA}79OVLp)`_*Y
zw)WPav3mtYMV(@Ym=?7Fy;!;%vPMQmNCbSeAU$gYJKFGzMyN=WpW%&4v(>(JaPY)?
z6aW2b7XuR$=dU%setzF)7f$Ou7GTh?wYBX6t`j66+UVR8QrWDODZWTT0EetMA|)kd
zM^%N~s9+B(-|O)_|36&;M{~^hk#93cGx}uG(zBC<R^F>O&#GX3#;v^b^JhfJ$(rp@
z;OWa7ZWX?3rm}*LU4I_lJv7Q4<BMFD`E>esO|y9HTrrrZH6x4)5dLv=dCY!~!peMe
zCtN!_Xcgnu%3DvXFjJRqdZ+?uYzaP-MiExCJiBhTY*UXF{u-<s5IE|4?CMHU-n`G=
zzEtro)R1mbTon%#Zl)kIJo^{38hto-&%U(TbF-30{<-4c`JS_NclCBe)EPurVwZ+r
address@hidden@?WqCU|
address@hidden<_;^j8i>8wHn8luoLFuwlv0m>34cBn)x0O86$VC~C
z$tABIE{96<s+TycLZP;^(e#!`fTqU843^SLRFmD-MBbWK!{i<b8O>s8{rpS852$_>
zCbdi^70q^irtyV^>yV~4iQyFnmY0c!di2fU96Gn|+<%lju9IcKSkptVMlV0)IFzH{
z545*b!7bub?WiDaVqB&engxFakujK3QhtSj+#Qr4C$=dg3fF(Zmi)address@hidden
znV`c84_nQ<address@hidden@t84-{-zaB*as~K3RKk8y^1^4yhj#zH+n$ATi;`^
address@hidden||address@hidden;
z4?8+K{7;VvKgNUVbg}(E;LyOpfTl|address@hidden@8Kl}TA
z+(8yay2^AbSKM)aHlVGSfvI4%4Wh3!M+wqEi+)W%E~u=2$-PkkBS|KO5~~v$u!923
address@hidden<&4y*6^M2q+G;Kw$lxM9#S#8
zF7jL$mPmZ`Up_LSTe4izx$C!(mVORFhGw?jvm1!pRaj*PepmjwPP5_PTD+Ndh(Jo4
zXUJ}rjzF#xrBa~}pizL=?~Jv}3S#s9yW&VWE+jrvKetys{rwSh9z!qfB(=l;Hor5b
address@hidden&<address@hidden)$UJed
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
address@hidden<address@hidden@M$|*8
address@hidden)l(!)(Slqw9<LX88ZsRlZU(address@hidden@Lmt11l`b45
zt6A|S$!-tR%&e1!I^sh0*9|fI4S)SAG^D_SfQ>w40)bSY7FD#AQo?^$CF1X&f!Hqb
zx$bC#m&`e#^<8e%6{tP{$>5L|address@hidden<|Y+3^F^Zb>%NvT%2ZgQ`OY=%UtN
zdl)Y6$Q4Pd2mmA?{qLc4-jYx7mwy4|7NFw}N3Q`_EN&address@hidden<&RN84|
address@hidden>{P?yx79X+)!g+XOU;B>$!DP7FhSdX&F?vPiIV>wf8?_p6+$
zJC=UJmCVzk7B)3}eQR|irK-h}G_lJv;address@hidden;2-KAhludCg)Dk
z<PVXQlKL~0sr!Ib2}5(JOVd<NAi*sfi4b&k)s->)Vm^zOtN40hEof0lRMf`Q!U7Z1
zv1Cq-)$DW!;cj?1{Ku&1&hKP!uK}DYPfUs&|5$q!3gqSB0cJDjk#44*q`4$R11unj
address@hidden)Fs1!E|Gf6eLwdlkJ1p;qfB0rvCr>address@hidden
z+n7UI&!l5ID%B|>Ku8VX-^oQtjThr*J4(~Mr<}S+rc9TpT?E)b{Pyh^n*8}6D2ruY
z{0jxQB$|qgi#vSq#RTbE;z$Zwq_{*48eqtqMD#MzzZ%9FB<Agt_YG3u)6&z;Y{1H_
address@hidden)$2W^tfm`C92<address@hidden>nt?+zU$R76%9ooQc1D**M3`
zUZ*buU%qV6MyZg=yvQ=tY(fO7!#t!3SDj!s#;|+wt3p)*-=(address@hidden>3lw+@
zLqkJbbMwP1O>address@hidden@hoZG~x%+oIPS5<B5zgVAo*$c-GIY~tHI#mgB
z(6q9~c8Xr*moJ5~hrWAtawO;U|7H{5W+ite8(i69W?>e#Sm~Lv7EP}&EObg8BU<%V
zxij}yF;jIgW%g7e^MzhehHn|address@hidden<B3kCzi8
zd*NbohcaXQu*u^pslYE7B6aFJKEI|ppqj6inwAVXRLTGgoMO*L$c|5l&Cj4igy{&R
zBt4YE03D4ZefX)Xg~3Ug4?Qk<&address@hidden)QWtgFeINGRjS4J#UJJp6p
address@hidden>iW2QAAn2r8`i#6(address@hidden;OLPXejGi&-YYI7pR?~Z~-
address@hidden)Z>(2+>(xV(address@hidden@<D%Q9X<qnR4G8VlHZP(l)=R
o9nTQSQFl__3Uv_%gZ`a$UC&GSjH?y}_d_8qH9gfb6}#~N0dko?5&address@hidden

literal 0
HcmV?d00001

diff --git a/www/media/job-error-obsolete.png b/www/media/job-error-obsolete.png
new file mode 100644
index 
0000000000000000000000000000000000000000..ef89d8f8e2c85c45e493c3b7cba4c23087e4dfe7
GIT binary patch
literal 5007
zcmWky2|address@hidden@?4B3fLA^R2?TedIKU{IFqYqt0-DSM42%ScF*
zWyms?>address@hidden@BZ%czwh4jKj+*R#zxxAj9iQW05I$7Xx;%!yT2Ju2fpVr
address@hidden)K}%^0Si|70WAPa5KKeI9FsN5SU?shuuHh~E
address@hidden|0x;xo30Mat};address@hidden&(;Gf64s5hSpTg6f
address@hidden(h;zaI_Ue`Zfcq+k%f**=-hLGYoD*q;address@hidden
zvV0zev!C+#_muUpc>>GEy{q+a`&Sg!+kTr3`;VR-w5_&`5_y|6>Te|address@hidden<*A9
zGJ0-sTpzLspD5dPe_W>b_uH!0>=qjIxrg4~)#r%)5P60f7>*sEI1jvcUzX}(#3$Z{
zY~Aca9`z?BB|QoR$^<E?gSI00V<B9B269t3_0ID`xH<w2SFgFRS#uxe$P`zjS8517
zRK;zc3*Y)RFkn1Kpr1LW?k=R<R;BXbY(?=QtmrSj3*Pc^c9PH00ysNKxZ#ihskY-#
zx+fYtA!*3I;>QKb8#6{wUT8u)9mr2T@(|L{nS7Xf(9?$xzcf_<@address@hidden
z^G@|*p^JE~DFn*HM~@;address@hidden@YHD|Rl*-VTdp-VWl4jg8e2W{Z>R
zl29ZekCaR}^7*;AEGfglX9uZVseNIbnlN4yATIB|uB&5LQBi?McO(k;D5&Q57a607
zl1#`b$dHVe!|`<address@hidden|HWe}b}`97#OLIX|F^zAp_Cr)iZ8(7
z#p2)n;x^3<)o+2Ixfi0J&93f&vn;u2jr#K__`<BMt&address@hidden
z&oeYUth{{d=wv{KIi?T*m^z4`K0#HF)^3PNNPNsav_(d3ueoTNIkEqD>dNUTfxv9g
z;RuAAJgOl`aL7bwP6AdbKrz4nUh0}l59!La7^wB=>s3V%vs?D_?)l8jOyb}mo0><A
z)cyx>address@hidden>^$cffKjip4Ft<xZ#A}g<hG=w?jm0k3G&RL&3Yn1qGjnXsV6VIc
z9LR>Z3ye=p<ho|Rdet#Dh8$dxQCmyKPPCnTy;address@hidden
zqf6YXvD_pQ>HPd#-TKj1%ki1cEq4LQE^w`tt79JnC1C1k)%@<^xA%PZewp7-rv+}r
z)address@hidden(II-VHVUhHNQ}5l#9{UrbiX)address@hidden(address@hidden
ztX9kse6Vp#o`=qn37qfna6TbPuNWwR&m_>%(d7j%Wx0+KU;-CoRhVZufw6ioez3Y=
zRncJR^YO8=Z{ve0Cal(<SG2D~^2JC--if3|@5Qed3~t}vojOA>(X~C9{S-MzfXqxb
zJ`Q)b=%+bU0pvrE<zMnD(address@hidden>address@hidden))PQBfao
zq*J~suBh$KyPCR~*vvCtpiLL9{_L6BOFk7w6ONjOhUY=bHrtDGY`DEV79h)jE)oN2
zw?oW&BeP5F-P{-mSpG|}+1-<qlS|s#+UKn3h}7NXp5x==X=u;`1T06<address@hidden
zS68P~e24i})?KM8b2Ai2VviHJO3Z5nC>JkXvbPw4bdB9UI5=oiR#bGyG63uA>(juN
ztlpK4#$~>>={9~q^2Uw%v7=l7U9oDF9_gnTZzuWlVaMbokH>_n)5QL4SXh{TQBjeH
zDdc&lL)*C-ZeU<~Iw3EQ4<P>jEyEaz8G0G;(G3utAb_gD3Wwzos;hynv8lKkLQPH0
z=uKu^dwcs4gWwqx`ngt-e8{0BsyH(%E6g6b8so<W!69nj%t&@68kOdXjW6%o+uMgh
z8q~PrU&bdS*iyU!hnoA`f(wSg`qb3a&(+lkVcmw%6Gd<bH3mPX$PlqFYboz&!m$%h
z&d#*>#OTeY)ymgoGI?05Qu|d<)FlWH6AUW<?~bsOMc3qHj;VPwZ<#U5E+F7yA!_bR
z(m#I>C4l#sINfM`yu83nW9RL?r4R5evL;M#Y6~TyrL!i<OciNYf=zC|>IC)4%={L(
zk5$z_9$Acx%6*1)f~<nrQ32Z7MUCvvPERD6XdXX)EZ2g*W-y?Y`PwD(L9A~h57<ip
address@hidden|Aki3G$%j5?V}HL#|address@hidden|c_Sb2E
zG{jDbzTv>address@hidden
address@hidden)a92cCW+e9rgOL2JyW1n^sY|6+P~yzYj3AJ}
zin;address@hidden>_?uf6H3=R76J7J}>+k;tk#U+caX84u{zB2OL~oMHwP7
zUzT?_s%}|11T^EyV0|uGm6+E!Cus)5p`3DbkFN|1ky7V+FsGF^loUGJy?VX=Bxhx!
address@hidden|YVNOzzM{iP?Oz63_xA(X&PSV|IDRFc?v2dJkrLiB%7^BH
z!<t>CZJSQ0tQ2oOsvmI-52XPVm6QZ^*+xc22nJ1D_IIznFSO8ZZE6CQj7*{Q^aro~
zUKzU60djv$3b2tP%#fr5!y`_yex%F^JuC%PmquCy$&WlT5``83Fglva|7mBFaS}9W
zVYaaTWevsLB^4M!hQn{-X%&IjHxdM(n>;`!x94tWaQ}r1_nx0`LdE0_PJg}h!(d7F
z7dO|QeUB4cxPSoLi!DQze5F96GH#qU|I_!m2;Z1HF~%sE-b#s?k{k{GuKbcaS2t2a
zH6JvKa2Rxp8Ui2)Vyi!zAO)$X%0T(address@hidden@address@hidden(s
z4ci8uGdw{wkP}Q1FsC|address@hidden&tjG`IfZX7b&EidOQ%ERQ4ZAe_oygdAcJIn
zU14<mUPbDIM`tdg0ToxxL2CpL2MzQWxv8n=bUgHcxw-lkY}qeL9LL=o%oQ)QEjy1b
zow7yXf*`gB#3qNDOdEFG4c9(A#LrkSH}q%`XyAWmX&oguL3l;z?1k3Y0Ni_*7LUY8
z;cnT8TVxfquq8X7jwmF?CtV8{0L|-d`es&%F^>iEnpFJZOP3$Rl1#>Wvj<1i634wm
zocMX5O{a)(yxq;OEyLC6Sy-C)r2NW_g<Wb6s{*h;N`o%oh=CFf2W`b^_s%DeVa>e6
z+yA{+9tTBN^NI3^WffWh4I1$boDx$FTHYhHxW3zSeNIZLECE934g2?Lz?QT+qnw0|
z(XRkmrCxmbgCJ`Fwf7JmmEW9}e;5#<P?-9OfUUpK^Sub-R;d~%CHEvSb81FvGNz$K
zS7(?8&DJ1Qn3Lcj#-e_WdBtZVS<l|td65A994orWMMS=31{}rOT?J~yNLh*nNy>IX
address@hidden|address@hidden)address@hidden>address@hidden|cx_6q$8{{?7Hhh5BmKLOyX?RO
address@hidden&D4QtSf4BZ{<S~m6*J~-3_hNX|F&`A*;`Ip>n6V|EHm8?&P}?MnX{Qzhz$MW
zI(riHZ|@)address@hidden@m^2+!B$-7ikkpdr;M9qLOI2rNnszw=~B
zF~6<U-1^(address@hidden(0!Ek&pGyKD>S{SY!jJ0$Vy)(=aHGa8Mtv
zrnw(39v-WB8XtPKKG+1-dS!Ie7(F8~vn63K03P50FXgm*^;i!$R>GD3dmC=UiCwxp
zWg2N7!!b$RYMeRTTwgkebd)q4%F-;q^CpibQAhZYa8||*p1d=q4ASBbwb>m>C#)Ne
zijmEtFi{BW-}aVng$u|Rq(%(G(Y>LT3qH+m*+y?^h0fTzmNC8zdllVxn{>o32D1|{
zJqZqGJs(|DTU%?dGN0|7H}SbKW0Jq|#H^9UCnJt}PbsiYjMNj!bs1!ljSWvHY0kL#
zcnEP}YisMZmH`6km;o2Gy>DfuA<q*vy;;RTbQyi|OUlPeJq%6Ey7YoR<AS00g^6Qn
address@hidden(=t3ET0_x4<;Jnq^2OT5(5DdFGftBECU0(gNe
z1K+lriqg_MCLKUSl9jbJt>address@hidden(yYcZ2$d`B?l=zIo|%;;YR-5K<i
z$P%}Y;U?VndMBZ3!nzpo3=QclCm_y!sZ<0g{<address@hidden&address@hidden
zuh&=tL3PebNOs-5k;NpP#e^yb=|w5qgj+qdjlWjxpZtDDEH-|&OZeIJJAWHPASx<q
z5TDov#T6G{);@Q&address@hidden
zIYh?X`RO)BIPp+f9=Y7*=;%moZEbZ1+Om6Hz&1HqF7=K=?=2$bXS!s*gyfM|R^0j$
z)address@hidden(address@hidden@dtXp4QkS`$vM~
zn;H-Llr{^hsx~zM+bbN5W+oh>N`1?__0A&AU~8RZe28iSg<{c4eW9Nre&R)fpt1`8
zf)T<address@hidden<l!tbJ;oxPpMoUycTW*t`$+2?{!FjCx;2=4N4H3SlR%raRk*hW>c-
z)T<PREz&hNhefDMc1fzK_3NL%$JB8c5%o`*3*SsTJ^$MINDLT9__=2<#YG#I4%L5S
address@hidden<f{?)VS+jwLfy#9d{|D(=?MGt}+t+cLFR8&;`eRSe#IFt$!WO+I0
z!>2pTnrlp#qe{rnqgk4)tgJe|eq}^D28g6kzphrbo!VXwBgioV5IMz+t3c|TH*Xeo
zgm>PQm$x|}qlo6FCLGIkQ=USgOW-ZPW<VjHx&4#UqkPiE?ZuSA>sikQK+)olzJCAy
zeXaHB(b7rinFci^PqM23#VD)`E%qD56_j7~JLx)X(iEDNR7=x)address@hidden
z;LIGF%vx1tWo0>EXtI@;s~V#?;Bwj<address@hidden|Cnnyt12ZAaZtEQUBvf
zd#87mOFb00<address@hidden)^EXOSy^V-;(j(R0#&hPh>t}q<>X|{S-TjSX4
z9#CCeT-+9Vv|cZfDwQoUwR-m&P{8-?5~;qoudnY#q5O}Lk$s%Ah40N*{%o?Ulm6F8
z7x&x+H^~L7O*gcj`1mBT+?8{6amfolJ2^NT?zH>p(Go&address@hidden@Qo^
zu?Qu7(baR~marw&GMRAjx|NqOTe($&address@hidden(~>P2aud6LeVdOh7W8)eAGC&0BiJ6t
z78eIkwe-5D(Nz!neE;ySA<sah&eYU&^Og{+wfXQr)DXVL2Ypl)oegexNDXC4Ye?o{
zHl5xTFcO&CZWuZzoYHf=)pGD7IKb1>address@hidden
za`g`b;4YF9hMh<hUMXqgp41c2G{H}AMo(`B;y<0-zD>address@hidden
zW1LqzYxf*<gRIgSNEfp2IOne&97P;address@hidden&M*3%nZ^IS2$g>m!cxnulS$3
z4ytg-z3LT8)7w6((-hi{NKpOrlOMdcm<QtMtX{*=Bz;z2Uc(*B&6O1eznz7iZW%-F
znS)`O-Xv8c<&>qCLCaZoO_Sy~K36Zs&g5HCpYBkPKZA*sANpryF<FzlviM_`IZ&nq
zaWA<jJnTTY*GhcGuju#lC%H(QA3QY&J4gD-Fmv<SU8?8Gro|8Efwu(PH>Bp#>B$|=
zidxE=;e|<address@hidden;{ZE*l?iMqyf9l&AyJCHk^RiL_;wql3*&m4LOe
zj;2aA0eJY*OU<C0z4}u*J-uw3z2DBbMrl0-bB&(KD=1LML4ZNDM;o-7mnFIGj_&Q(
address@hidden(u^D=Aibs%nQ+9t0Z14?kysUpvK4m(;^|@chuxc{G
zxsGyeJLotU3~KnDq_h08=4DjUhqK8KA(address@hidden|7E<LBvLu#aEq9y5^Us_
zeN%ws>vb)address@hidden@fjEByM<lm#;C9x|6
zKHdS{#Qy^Q{rB)address@hidden
zDjix`p;D<mLCUR`=`}lIAMQvm4$A}AD?WJnY2SVgS;`O%e2>XfFITR=C-u#`=!(RJ
z)cV5i!3PO1DDFVBL7MR(libiX2>L28v$yt*1||WNLrempA50-->CVAv^34F6mkVG4
zGU?|jF4Dmtp#y8XcLxCvS<address@hidden@Qmi^xtoa5Xv=Z%whYj{Wu#e&u?znng#c&U

literal 0
HcmV?d00001

diff --git a/www/media/style.css b/www/media/style.css
index f24525e..7dd9432 100644
--- a/www/media/style.css
+++ b/www/media/style.css
@@ -269,6 +269,10 @@ table.jobinfo td.info {
   vertical-align: top;
 }
 
+p.nofiles {
+  font-style: italic;
+}
+
 div.mapsearch {
   float: right;
   font-style: italic;
diff --git a/www/templates/maposmatic/job-page.html 
b/www/templates/maposmatic/job-page.html
index ff3bf5b..bc55b2c 100644
--- a/www/templates/maposmatic/job-page.html
+++ b/www/templates/maposmatic/job-page.html
@@ -44,7 +44,7 @@
 
 {% include "maposmatic/job.html" %}
 
-{% if not job.is_done %}
+{% if job.needs_waiting %}
 <p>
 &raquo; <a href="{% url job-by-id job.id %}">{% trans "Refresh the status" 
%}</a> {% blocktrans %}(the page will refresh automatically every {{ refresh }} 
seconds until the rendering is completed).{% endblocktrans %}
 </p>
diff --git a/www/templates/maposmatic/job.html 
b/www/templates/maposmatic/job.html
index cd181d6..4193b99 100644
--- a/www/templates/maposmatic/job.html
+++ b/www/templates/maposmatic/job.html
@@ -31,7 +31,7 @@
 {% if not single %}<h2 class="jobtitle"><a href="{% url job-by-id job.id 
%}">{{ job.maptitle }}</a></h2>{% endif %}
 <table class="jobinfo"><tbody><tr>
   <td class="status">
-    <img {% if job.is_done_ok %}src="/smedia/job-done.png"{% else %}{% if 
job.is_done_failed %}src="/smedia/job-error.png"{% else %}{% if job.is_waiting 
%}src="/smedia/job-in-queue.png"{% else %}src="/smedia/job-in-progress.png"{% 
endif %}{% endif %}{% endif %} title="{{ 
job.status|job_status_to_str:job.resultmsg }}" />
+    <img src="/smedia/{{ job.status|job_status_to_icon_name:job.resultmsg 
}}.png" title="{{ job.status|job_status_to_str:job.resultmsg }} ({{ job.status 
}})" />
   </td>
   <td class="info">
     {% if job.administrative_city %}
@@ -43,28 +43,21 @@
 
     <h4>{% trans "Rendering: " %}</h4>
     {% trans "Rendering submitted" %} {{ job.submission_time|date:"l d M Y\, 
H:i:s" }}.<br />
-    {% if job.is_waiting %}
-      {% trans "In queue, position" %} {{ job.current_position_in_queue }}.
-    {% else %}{% if job.is_done %}{% if job.is_done_ok %}
-      {% trans "Completed on" %}
-    {% else %}
-      {% trans "Failed on" %}
-    {% endif %}
-    {{ job.endofrendering_time|date:"l d M Y\, H:i:s" }} ({% blocktrans with 
job.startofrendering_time|timesince:job.endofrendering_time as rendering 
%}rendering took {{ rendering }}{% endblocktrans %}).
-    {% else %}
-      {% trans "Rendering in progress..." %}
-    {% endif %}
-    {% endif %}
+    {% if job.is_waiting %}{% trans "In queue, position" %} {{ 
job.current_position_in_queue }}.{% endif %}
+    {% if job.is_rendering %}{% trans "Rendering in progress..." %}{% endif %}
+    {% if not job.needs_waiting %}
+      {% if job.is_done_ok or job.is_obsolete_ok %}{% trans "Completed on" 
%}{% endif %}
+      {% if job.is_done_failed or job.is_obsolete_failed %}{% trans "Failed 
on" %}{% endif %}
+      {{ job.endofrendering_time|date:"l d M Y\, H:i:s" }}{% if 
job.rendering_time_gt_1min %} ({% blocktrans with 
job.startofrendering_time|timesince:job.endofrendering_time as rendering 
%}rendering took {{ rendering }}{% endblocktrans %}){% endif %}.
 
-    {% if job.is_done_ok %}
     {% if job.has_output_files %}
     <h4>{% trans "Files: " %}</h4>
     <ul>
-      <li>{% trans "Map: " %} {% for file in job.output_files.maps %}<a 
href="{{ file.1 }}" title="{{ file.2 }}">{{ file.0|upper }}</a>{% if not 
forloop.last %}, {% endif %}{% endfor %}.</li>
-      <li>{% trans "Index: " %} {% for file in job.output_files.indeces %}<a 
href="{{ file.1 }}" title="{{ file.2 }}">{{ file.0|upper }}</a>{% if not 
forloop.last %}, {% endif %}{% endfor %}.</li>
+      <li>{% trans "Map: " %} {% for file in job.output_files.maps %}<a 
href="{{ file.1 }}" title="{{ file.2 }}">{{ file.0|upper }}</a> ({{ 
file.3|filesizeformat }}){% if not forloop.last %}, {% endif %}{% endfor 
%}.</li>
+      <li>{% trans "Index: " %} {% for file in job.output_files.indeces %}<a 
href="{{ file.1 }}" title="{{ file.2 }}">{{ file.0|upper }}</a> ({{ 
file.3|filesizeformat }}){% if not forloop.last %}, {% endif %}{% endfor 
%}.</li>
     </ul>
     {% else %}
-      {% trans "The generated files are no longer available." %}
+      <p class="nofiles">{% trans "The generated files are no longer 
available." %}</p>
     {% endif %}
     {% endif %}
   </td>
-- 
1.6.3.3.277.g88938c





reply via email to

[Prev in Thread] Current Thread [Next in Thread]