From 39a548fd2629f3b6383990264b2e331b3aea99fb Mon Sep 17 00:00:00 2001 From: eliott Date: Sat, 3 Nov 2007 03:45:10 -0400 Subject: Initial import for public release... Special Note Prior to git import, approx 90% of the code was done by Judd Vinet. Thanks Judd! --- .gitignore | 4 + AUTHORS | 6 + LICENSE | 339 +++++ README | 14 + __init__.py | 0 common/__init__.py | 0 common/models.py | 50 + common/templatetags/__init__.py | 0 common/templatetags/validation.py | 12 + data/pkgmaint_guide.txt | 164 +++ devel/__init__.py | 0 devel/views.py | 67 + feeds.py | 29 + lib/__init__.py | 0 lib/markdown.py | 1874 +++++++++++++++++++++++++ local_settings.py.example | 33 + manage.py | 11 + media/PythonPowered.png | Bin 0 -> 945 bytes media/arch.css | 384 ++++++ media/calendar.css | 265 ++++ media/calendar.js | 2133 +++++++++++++++++++++++++++++ media/favicon.ico | Bin 0 -> 1150 bytes media/forms/_library/cmxform.css | 57 + media/forms/cmxform.css | 30 + media/forms/cmxform.js | 22 + media/forms/core.css | 22 + media/forms/images/cmxform-divider.gif | Bin 0 -> 43 bytes media/forms/images/cmxform-fieldset.gif | Bin 0 -> 2926 bytes media/forms/reset.css | 62 + media/forms/screen.css | 16 + media/gnu-head-tiny.jpg | Bin 0 -> 3049 bytes media/jquery.js | 14 + media/logo.png | Bin 0 -> 15730 bytes media/mailman-large.jpg | Bin 0 -> 6150 bytes media/mailman.jpg | Bin 0 -> 2022 bytes media/mm-icon.png | Bin 0 -> 281 bytes media/rss.png | Bin 0 -> 725 bytes media/tab.png | Bin 0 -> 107 bytes media/title.png | Bin 0 -> 6465 bytes media/title_back.png | Bin 0 -> 168 bytes news/__init__.py | 0 news/models.py | 19 + news/views.py | 82 ++ packages/__init__.py | 0 packages/models.py | 91 ++ packages/templatetags/__init__.py | 0 packages/templatetags/package_extras.py | 28 + packages/views.py | 172 +++ public/__init__.py | 0 public/views.py | 56 + scripts/daily_cleanup.py | 14 + scripts/djangoshell.sh | 6 + settings.py | 89 ++ templates/403.html | 9 + templates/404.html | 9 + templates/500.html | 9 + templates/base.html | 84 ++ templates/devel/index.html | 75 + templates/devel/profile.html | 32 + templates/error_page.html | 8 + templates/errors.html | 5 + templates/feeds/news_description.html | 1 + templates/feeds/news_title.html | 1 + templates/feeds/packages_description.html | 1 + templates/feeds/packages_title.html | 1 + templates/news/add.html | 26 + templates/news/delete.html | 16 + templates/news/list.html | 26 + templates/news/view.html | 13 + templates/packages/details.html | 77 ++ templates/packages/files.html | 11 + templates/packages/flag.html | 26 + templates/packages/flaghelp.html | 14 + templates/packages/outofdate.txt | 13 + templates/packages/search.html | 120 ++ templates/public/about.html | 82 ++ templates/public/art.html | 28 + templates/public/blank.html | 10 + templates/public/cvs.html | 48 + templates/public/denied.html | 11 + templates/public/developers.html | 67 + templates/public/donate.html | 59 + templates/public/download.html | 74 + templates/public/index.html | 174 +++ templates/public/index.html.bak | 151 ++ templates/public/irc.html | 39 + templates/public/moreforums.html | 78 ++ templates/public/press.html | 33 + templates/public/projects.html | 36 + templates/registration/login.html | 23 + templates/registration/logout.html | 10 + templates/status_page.html | 8 + templates/todolists/add.html | 25 + templates/todolists/list.html | 30 + templates/todolists/view.html | 31 + templates/wiki/base.html | 30 + templates/wiki/edit.html | 22 + templates/wiki/home.html | 9 + templates/wiki/page.html | 17 + todolists/__init__.py | 0 todolists/models.py | 31 + todolists/views.py | 64 + urls.py | 71 + utils.py | 69 + wiki/__init__.py | 0 wiki/models.py | 16 + wiki/templatetags/__init__.py | 0 wiki/templatetags/wikitags.py | 57 + wiki/views.py | 61 + 109 files changed, 8206 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 README create mode 100644 __init__.py create mode 100644 common/__init__.py create mode 100644 common/models.py create mode 100644 common/templatetags/__init__.py create mode 100644 common/templatetags/validation.py create mode 100644 data/pkgmaint_guide.txt create mode 100644 devel/__init__.py create mode 100644 devel/views.py create mode 100644 feeds.py create mode 100644 lib/__init__.py create mode 100644 lib/markdown.py create mode 100644 local_settings.py.example create mode 100755 manage.py create mode 100644 media/PythonPowered.png create mode 100644 media/arch.css create mode 100644 media/calendar.css create mode 100644 media/calendar.js create mode 100644 media/favicon.ico create mode 100644 media/forms/_library/cmxform.css create mode 100644 media/forms/cmxform.css create mode 100644 media/forms/cmxform.js create mode 100644 media/forms/core.css create mode 100644 media/forms/images/cmxform-divider.gif create mode 100644 media/forms/images/cmxform-fieldset.gif create mode 100644 media/forms/reset.css create mode 100644 media/forms/screen.css create mode 100644 media/gnu-head-tiny.jpg create mode 100644 media/jquery.js create mode 100644 media/logo.png create mode 100644 media/mailman-large.jpg create mode 100644 media/mailman.jpg create mode 100644 media/mm-icon.png create mode 100644 media/rss.png create mode 100644 media/tab.png create mode 100644 media/title.png create mode 100644 media/title_back.png create mode 100644 news/__init__.py create mode 100644 news/models.py create mode 100644 news/views.py create mode 100644 packages/__init__.py create mode 100644 packages/models.py create mode 100644 packages/templatetags/__init__.py create mode 100644 packages/templatetags/package_extras.py create mode 100644 packages/views.py create mode 100644 public/__init__.py create mode 100644 public/views.py create mode 100644 scripts/daily_cleanup.py create mode 100755 scripts/djangoshell.sh create mode 100644 settings.py create mode 100644 templates/403.html create mode 100644 templates/404.html create mode 100644 templates/500.html create mode 100644 templates/base.html create mode 100644 templates/devel/index.html create mode 100644 templates/devel/profile.html create mode 100644 templates/error_page.html create mode 100644 templates/errors.html create mode 100644 templates/feeds/news_description.html create mode 100644 templates/feeds/news_title.html create mode 100644 templates/feeds/packages_description.html create mode 100644 templates/feeds/packages_title.html create mode 100644 templates/news/add.html create mode 100644 templates/news/delete.html create mode 100644 templates/news/list.html create mode 100644 templates/news/view.html create mode 100644 templates/packages/details.html create mode 100644 templates/packages/files.html create mode 100644 templates/packages/flag.html create mode 100644 templates/packages/flaghelp.html create mode 100644 templates/packages/outofdate.txt create mode 100644 templates/packages/search.html create mode 100644 templates/public/about.html create mode 100644 templates/public/art.html create mode 100644 templates/public/blank.html create mode 100644 templates/public/cvs.html create mode 100644 templates/public/denied.html create mode 100644 templates/public/developers.html create mode 100644 templates/public/donate.html create mode 100644 templates/public/download.html create mode 100644 templates/public/index.html create mode 100644 templates/public/index.html.bak create mode 100644 templates/public/irc.html create mode 100644 templates/public/moreforums.html create mode 100644 templates/public/press.html create mode 100644 templates/public/projects.html create mode 100644 templates/registration/login.html create mode 100644 templates/registration/logout.html create mode 100644 templates/status_page.html create mode 100644 templates/todolists/add.html create mode 100644 templates/todolists/list.html create mode 100644 templates/todolists/view.html create mode 100644 templates/wiki/base.html create mode 100644 templates/wiki/edit.html create mode 100644 templates/wiki/home.html create mode 100644 templates/wiki/page.html create mode 100644 todolists/__init__.py create mode 100644 todolists/models.py create mode 100644 todolists/views.py create mode 100644 urls.py create mode 100644 utils.py create mode 100644 wiki/__init__.py create mode 100644 wiki/models.py create mode 100644 wiki/templatetags/__init__.py create mode 100644 wiki/templatetags/wikitags.py create mode 100644 wiki/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33b2f64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +*.swp +local_settings.py + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..3917c18 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +# AUTHORS +Judd Vinet +Simo Leone +eliott + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README b/README new file mode 100644 index 0000000..b0067e8 --- /dev/null +++ b/README @@ -0,0 +1,14 @@ +# License + See LICENSE file. + +# Authors + See AUTHORS file. + +# Installation + +## Dependencies + - python + - mysql-python + - Django >= 0.95 + + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/models.py b/common/models.py new file mode 100644 index 0000000..883675b --- /dev/null +++ b/common/models.py @@ -0,0 +1,50 @@ +from django.db import models +from django.contrib.auth.models import User + +class Mirror(models.Model): + id = models.AutoField(primary_key=True) + domain = models.CharField(maxlength=255) + country = models.CharField(maxlength=255) + url = models.CharField(maxlength=255) + protocol_list = models.CharField(maxlength=255, null=True, blank=True) + admin_email = models.CharField(maxlength=255, null=True, blank=True) + def __str__(self): + return self.domain + class Admin: + list_display = ('domain', 'country') + list_filter = ('country',) + ordering = ['domain'] + search_fields = ('domain') + pass + +class Donator(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(maxlength=255) + def __str__(self): + return self.name + class Admin: + ordering = ['name'] + search_fields = ('name') + pass + +class UserProfile(models.Model): + id = models.AutoField(primary_key=True) # not technically needed + notify = models.BooleanField("Send notifications", default=True, help_text="When enabled, user will recieve 'flag out of date' notifications") + alias = models.CharField(core=True, maxlength=50, help_text="Required field") + public_email = models.CharField(core=True, maxlength=50, help_text="Required field") + other_contact = models.CharField(maxlength=100, null=True, blank=True) + website = models.URLField(null=True, blank=True) + yob = models.IntegerField(null=True, blank=True) + location = models.CharField(maxlength=50, null=True, blank=True) + languages = models.CharField(maxlength=50, null=True, blank=True) + interests = models.CharField(maxlength=255, null=True, blank=True) + occupation = models.CharField(maxlength=50, null=True, blank=True) + roles = models.CharField(maxlength=255, null=True, blank=True) + favorite_distros = models.CharField(maxlength=255, null=True, blank=True) + picture = models.FileField(upload_to='devs', default='devs/silhouette.png') + user = models.ForeignKey(User, edit_inline=models.STACKED, num_in_admin=1, min_num_in_admin=1, max_num_in_admin=1, num_extra_on_change=0, unique=True) + class Meta: + db_table = 'user_profiles' + verbose_name = 'Additional Profile Data' + verbose_name_plural = 'Additional Profile Data' + diff --git a/common/templatetags/__init__.py b/common/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/templatetags/validation.py b/common/templatetags/validation.py new file mode 100644 index 0000000..40c48da --- /dev/null +++ b/common/templatetags/validation.py @@ -0,0 +1,12 @@ +from django import template + +register = template.Library() + +@register.inclusion_tag('errors.html') +def print_errors(errors): + errs = [] + for e,msg in errors.iteritems(): + errmsg = str(msg[0]) + # hack -- I'm a python idiot + errs.append( (e, errmsg[2:-2]) ) + return {'errors': errs} diff --git a/data/pkgmaint_guide.txt b/data/pkgmaint_guide.txt new file mode 100644 index 0000000..85106ea --- /dev/null +++ b/data/pkgmaint_guide.txt @@ -0,0 +1,164 @@ +============================================================================= + THE QUICK AND DIRTY ON HOW TO BE A PACKAGE MAINTAINER +============================================================================= + questions to jvinet@zeroflux.org + +1. Follow Package Guidelines + + Package guidelines can be found in the Arch Linux documentation. + Please follow them closely. + +2. How To Use CVS + + The example commands below assume the module 'extra'. + + 2.1 Make sure your CVSROOT environment variable is set properly. If the + CVS repository is on the same box: + # export CVSROOT=/home/cvs-extra + + If you want to access the repository from the different box via SSH: + # export CVS_RSH=ssh + # export CVSROOT=:ext:user@cvs.archlinux.org:/home/cvs-extra + + 2.2 Checkout the repository. This will download the entire repository to + your local machine: + # cvs co extra + + 2.3 Updating the repository. This syncs your local repository with the + master. You should do this often, especially if other people could be + working on the same repository. + # cd extra + # cvs -q update -d + + 2.4 Adding files/directories to the repository. When you want to add a new + package you should create a directory under the respective category and + place the new PKGBUILD in it. For example, to add fvwm to the repo: + # cd extra/x11 + # mkdir fvwm + # cd fvwm + # cp /var/abs/PKGBUILD.proto ./PKGBUILD + + # cd .. + # cvs add fvwm + # cvs add fvwm/PKGBUILD + + 2.5 Committing changes. Files are not written to the master repository until + you commit. Never forget to commit! + # cd extra + # cvs commit + + This will find all modified files, then throw you into vi where you can + add a log message describing your changes. Save and exit from vi when + you're done and cvs will update the files in the master repository. + + 2.6 Removing files. If you need to remove a file (eg, an old patch that + isn't needed anymore), you can do the following: + # cd extra/x11/fvwm + # rm old.patch + # cvs remove old.patch + # cvs commit -m "removed old.patch" old.patch + also remove the CURRENT/STABLE tags from the file so it does not appear + in ABS any more: + # cvs tag -d CURRENT old.patch + + Don't forget to commit afterwards! Remember that no changes are made + to the master until you commit. + + 2.7 Removing directories cannot be done easily. If you really need to + remove a directory, email the sysadmin (Judd) and I'll help you out. + + 2.8 Tagging files. Every file in CVS has tags associated with it, which + allows us to select certain versions of scripts. The db generation + scripts will only look at files that are tagged as CURRENT, so you need + to tag all files after you commit them: + # cd extra/x11/fvwm + # cvs tag -c -F -R CURRENT + + NOTE: When tagging, you should be sure to ONLY tag the updated files, + not the entire repository. Otherwise, if parts of your checkout are + out-of-date, you may actually be tagging an OLDER version of a file, + reversing someone else's tag procedure. + +3. The Process + + 3.1 Checkout/update your local repository from cvs.archlinux.org + 3.2 Make any changes you need to + 3.3 Put your new packages in your local staging directory on archlinux.org. + Suggested syntax is: + scp name-ver-rel.pkg.tar.gz you@archlinux.org:staging/extra/add + 3.4 Commit your changes (section 2.5) + 3.5 Update the CURRENT tags to new revisions (section 2.8) + 3.6 Log in to archlinux.org and run the /arch/db-extra script, which + will re-generate the sync db and place it in /home/ftp/extra, then + move the new/updated packages from your staging directory to the + FTP tree. + 3.7 Remove any older versions of packages from /home/ftp/extra to + save diskspace, they should be noted when the db generation script + finishes. + + Make sure you do things in this order, mixing them up can break things + temporarily. For example, if you remove older versions from the ftp + tree before you update the database or update the database before + uploading new packages, arch users trying to download the package at + that time will get "file not found" errors. + +4. Staging Directories + + As mentioned in Section 3, packages need to be uploaded to the proper + staging directory before running a db generation script. The staging + area (located in your home dir) looks like so: + + staging + |-- arch + | |-- add + | `-- del + |-- extra + | |-- add + | `-- del + |-- testing + | |-- add + | `-- del + `-- unstable + |-- add + `-- del + + As you can see, each repository has two staging directories: "add" and + "del". When you want to add or update a package, you'll place it in the + "add" directory for the repository you're working in. Then run the db-gen + script. + + When you want to remove a package, you will move the package OUT OF the FTP + directory (eg, /home/ftp/extra/os/i686/) and INTO the "del" directory for + the repository you're working in. Once moved, you can run the db-gen + script -- it will see that the file has left the FTP tree and will remove + it from the package database. + +5. Miscellaneous Stuff + + 5.1 If you are creating a daemon you need to include an rc.d startup + script for it. Look at /var/abs/daemons/esd for a simple example. + 5.2 Please include a line that says '# $Id: pkgmaint_guide.txt,v 1.3 2006/10/05 20:52:01 judd Exp $' at the top of each + PKGBUILD. This will be parsed by cvs during a commit, and replaced + with user/timestamp data. + 5.3 Please do some rudimentary checks of the package before making it + 'live'. Try installing it and see if there are any file conflicts. + Check for dependencies by running 'ldd' against the binaries and + looking through the .so files it requires. For example, + 'ldd /usr/bin/gvim' returns a big list of libs, one of which is + libgtk-x11-2.0.so.0, so gtk2 should be one of the dependencies for + gvim. Also, namcap is available in the extra repository. Running it + against a package will print dependancy warnings as well as possible + configuration problems. Namcap is not the final word, if ldd or + runtime show otherwise, believe them instead. + 5.4 When creating a package description for a package, do not include + the package name in a self-referencing way, as it is redundant. + For example, "Nedit is a text editor for X11" could be simplified to + "A text editor for X11". Also try to keep the descriptions to ~80 + characters or less. + 5.5 When entering cvs log messages for new/upgraded packages, please use + these tags so they can be easily parsed for changelog generation: + if the package is upgrade use: 'upgpkg: pkgname newpkgver' + if the package is new use: 'newpkg: pkgname newpkgver' + + +$Id: pkgmaint_guide.txt,v 1.3 2006/10/05 20:52:01 judd Exp $ diff --git a/devel/__init__.py b/devel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devel/views.py b/devel/views.py new file mode 100644 index 0000000..dea652b --- /dev/null +++ b/devel/views.py @@ -0,0 +1,67 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core import validators +from archlinux.utils import render_template +from archlinux.packages.models import Package +from archlinux.todolists.models import Todolist, TodolistPkg +from archlinux.settings import DATA_DIR +from archlinux.utils import validate +from archlinux.common.models import UserProfile + +@login_required +def index(request): + try: + thismaint = User.objects.get(username=request.user.username) + except User.DoesNotExist: + # weird, we don't have a maintainer record for this logged-in user + thismaint = None + + # get a list of incomplete package todo lists + todos = Todolist.objects.get_incomplete() + # get flagged-package stats for all maintainers + stats = Package.objects.get_flag_stats() + if thismaint: + # get list of flagged packages for this maintainer + pkgs = Package.objects.filter(maintainer=thismaint.id).filter(needupdate=True).order_by('repo', 'pkgname') + else: + pkgs = None + + return render_template('devel/index.html', request, + {'stats':stats, 'pkgs':pkgs, 'todos':todos, 'maint':thismaint}) + +@login_required +#@is_maintainer +def change_notify(request): + maint = User.objects.get(username=request.user.username) + notify = request.POST.get('notify', 'no') + try: + maint.get_profile().notify = notify == 'yes' + except UserProfile.DoesNotExist: + UserProfile(user_id=maint.id ,notify=notify == 'yes').save() + maint.get_profile().save() + return HttpResponseRedirect('/devel/') + +@login_required +def change_profile(request): + errors = {} + if request.POST: + passwd1, passwd2 = request.POST['passwd'], request.POST['passwd2'] + email = request.POST['email'] + # validate + if passwd1 != passwd2: + errors['password'] = [' Passwords do not match. '] + validate(errors, 'Email', email, validators.isValidEmail, False, request) + # apply changes + if not errors: + request.user.email = email + if passwd1: + request.user.set_password(passwd1) + request.user.save() + return HttpResponseRedirect('/devel/') + return render_template('devel/profile.html', request, {'errors':errors,'email':request.user.email}) + +@login_required +def guide(request): + return HttpResponse(file(DATA_DIR + '/pkgmaint_guide.txt').read(), + mimetype='text/plain') diff --git a/feeds.py b/feeds.py new file mode 100644 index 0000000..bddf941 --- /dev/null +++ b/feeds.py @@ -0,0 +1,29 @@ +from django.contrib.syndication.feeds import Feed +from archlinux.packages.models import Package +from archlinux.news.models import News +#from datetime import datetime + +class PackageFeed(Feed): + title = 'Recent Package Updates' + link = '/packages/' + description = 'Recent Package Updates' + + def items(self): + return Package.objects.order_by('-last_update')[:10] + + def item_pubdate(self, item): + return item.last_update + + def item_categories(self, item): + return (item.repo.name,item.category.category) + +class NewsFeed(Feed): + title = 'Recent News Updates' + link = '/news/' + description = 'Recent News Updates' + + def items(self): + return News.objects.order_by('-postdate', '-id')[:10] + + def item_pubdate(self, item): + return item.postdate diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/markdown.py b/lib/markdown.py new file mode 100644 index 0000000..6b3e57a --- /dev/null +++ b/lib/markdown.py @@ -0,0 +1,1874 @@ +#!/usr/bin/env python + +SPEED_TEST = 0 + +""" +==================================================================== +IF YOU ARE LOOKING TO EXTEND MARKDOWN, SEE THE "FOOTNOTES" SECTION +==================================================================== + +Python-Markdown +=============== + +Converts Markdown to HTML. Basic usage as a module: + + import markdown + html = markdown.markdown(your_text_string) + +Started by [Manfred Stienstra](http://www.dwerg.net/). Continued and +maintained by [Yuri Takhteyev](http://www.freewisdom.org). + +Project website: http://www.freewisdom.org/projects/python-markdown +Contact: yuri [at] freewisdom.org + +License: GPL 2 (http://www.gnu.org/copyleft/gpl.html) or BSD + +Version: 1.5 (May 15, 2006) + +For changelog, see end of file +""" + +import re, sys, os, random + +# set debug level: 3 none, 2 critical, 1 informative, 0 all +(VERBOSE, INFO, CRITICAL, NONE) = range(4) + +MESSAGE_THRESHOLD = CRITICAL + +def message(level, text) : + if level >= MESSAGE_THRESHOLD : + print text + + +# --------------- CONSTANTS YOU MIGHT WANT TO MODIFY ----------------- + +# all tabs will be expanded to up to this many spaces +TAB_LENGTH = 4 +ENABLE_ATTRIBUTES = 1 +SMART_EMPHASIS = 1 + +# --------------- CONSTANTS YOU _SHOULD NOT_ HAVE TO CHANGE ---------- + +FN_BACKLINK_TEXT = "zz1337820767766393qq" +# a template for html placeholders +HTML_PLACEHOLDER_PREFIX = "qaodmasdkwaspemas" +HTML_PLACEHOLDER = HTML_PLACEHOLDER_PREFIX + "%dajkqlsmdqpakldnzsdfls" + +BLOCK_LEVEL_ELEMENTS = ['p', 'div', 'blockquote', 'pre', 'table', + 'dl', 'ol', 'ul', 'script', 'noscript', + 'form', 'fieldset', 'iframe', 'math', 'ins', + 'del', 'hr', 'hr/'] + +def is_block_level (tag) : + return ( (tag in BLOCK_LEVEL_ELEMENTS) or + (tag[0] == 'h' and tag[1] in "0123456789") ) + +""" +====================================================================== +========================== NANODOM =================================== +====================================================================== + +The three classes below implement some of the most basic DOM +methods. I use this instead of minidom because I need a simpler +functionality and do not want to require additional libraries. + +Importantly, NanoDom does not do normalization, which is what we +want. It also adds extra white space when converting DOM to string +""" + + +class Document : + + def appendChild(self, child) : + self.documentElement = child + child.parent = self + self.entities = {} + + def createElement(self, tag, textNode=None) : + el = Element(tag) + el.doc = self + if textNode : + el.appendChild(self.createTextNode(textNode)) + return el + + def createTextNode(self, text) : + node = TextNode(text) + node.doc = self + return node + + def createEntityReference(self, entity): + if entity not in self.entities: + self.entities[entity] = EntityReference(entity) + return self.entities[entity] + + def toxml (self) : + return self.documentElement.toxml() + + def normalizeEntities(self, text) : + + pairs = [ #("&", "&"), + ("<", "<"), + (">", ">"), + ("\"", """)] + + for old, new in pairs : + text = text.replace(old, new) + return text + + def find(self, test) : + return self.documentElement.find(test) + + def unlink(self) : + self.documentElement.unlink() + self.documentElement = None + + +class Element : + + type = "element" + + def __init__ (self, tag) : + + self.nodeName = tag + self.attributes = [] + self.attribute_values = {} + self.childNodes = [] + + def unlink(self) : + for child in self.childNodes : + if child.type == "element" : + child.unlink() + self.childNodes = None + + def setAttribute(self, attr, value) : + if not attr in self.attributes : + self.attributes.append(attr) + + self.attribute_values[attr] = value + + def insertChild(self, position, child) : + self.childNodes.insert(position, child) + child.parent = self + + def removeChild(self, child) : + self.childNodes.remove(child) + + def replaceChild(self, oldChild, newChild) : + position = self.childNodes.index(oldChild) + self.removeChild(oldChild) + self.insertChild(position, newChild) + + def appendChild(self, child) : + self.childNodes.append(child) + child.parent = self + + def handleAttributes(self) : + pass + + def find(self, test, depth=0) : + """ Returns a list of descendants that pass the test function """ + matched_nodes = [] + for child in self.childNodes : + if test(child) : + matched_nodes.append(child) + if child.type == "element" : + matched_nodes += child.find(test, depth+1) + return matched_nodes + + def toxml(self): + if ENABLE_ATTRIBUTES : + for child in self.childNodes: + child.handleAttributes() + buffer = "" + if self.nodeName in ['h1', 'h2', 'h3', 'h4'] : + buffer += "\n" + elif self.nodeName in ['li'] : + buffer += "\n " + buffer += "<" + self.nodeName + for attr in self.attributes : + value = self.attribute_values[attr] + value = self.doc.normalizeEntities(value) + buffer += ' %s="%s"' % (attr, value) + if self.childNodes or self.nodeName in ['blockquote']: + buffer += ">" + for child in self.childNodes : + buffer += child.toxml() + if self.nodeName == 'p' : + buffer += "\n" + elif self.nodeName == 'li' : + buffer += "\n " + buffer += "" % self.nodeName + else : + buffer += "/>" + if self.nodeName in ['p', 'li', 'ul', 'ol', + 'h1', 'h2', 'h3', 'h4'] : + buffer += "\n" + + return buffer + + +class TextNode : + + type = "text" + attrRegExp = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123} + + def __init__ (self, text) : + self.value = text + + def attributeCallback(self, match) : + self.parent.setAttribute(match.group(1), match.group(2)) + + def handleAttributes(self) : + self.value = self.attrRegExp.sub(self.attributeCallback, self.value) + + def toxml(self) : + text = self.value + if not text.startswith(HTML_PLACEHOLDER_PREFIX): + if self.parent.nodeName == "p" : + text = text.replace("\n", "\n ") + elif (self.parent.nodeName == "li" + and self.parent.childNodes[0]==self): + text = "\n " + text.replace("\n", "\n ") + text = self.doc.normalizeEntities(text) + return text + + +class EntityReference: + + type = "entity_ref" + + def __init__(self, entity): + self.entity = entity + + def handleAttributes(self): + pass + + def toxml(self): + return "&" + self.entity + ";" + + +""" +====================================================================== +========================== PRE-PROCESSORS ============================ +====================================================================== + +Preprocessors munge source text before we start doing anything too +complicated. + +Each preprocessor implements a "run" method that takes a pointer to +a list of lines of the document, modifies it as necessary and +returns either the same pointer or a pointer to a new list. +""" + +class HeaderPreprocessor : + + """ + Replaces underlined headers with hashed headers to avoid + the nead for lookahead later. + """ + + def run (self, lines) : + + for i in range(len(lines)) : + if not lines[i] : + continue + + if lines[i].startswith("#") : + lines.insert(i+1, "\n") + + if (i+1 <= len(lines) + and lines[i+1] + and lines[i+1][0] in ['-', '=']) : + + underline = lines[i+1].strip() + + if underline == "="*len(underline) : + lines[i] = "# " + lines[i].strip() + lines[i+1] = "" + elif underline == "-"*len(underline) : + lines[i] = "## " + lines[i].strip() + lines[i+1] = "" + + return lines + +HEADER_PREPROCESSOR = HeaderPreprocessor() + +class LinePreprocessor : + """Deals with HR lines (needs to be done before processing lists)""" + + def run (self, lines) : + for i in range(len(lines)) : + if self._isLine(lines[i]) : + lines[i] = "
" + return lines + + def _isLine(self, block) : + """Determines if a block should be replaced with an
""" + if block.startswith(" ") : return 0 # a code block + text = "".join([x for x in block if not x.isspace()]) + if len(text) <= 2 : + return 0 + for pattern in ['isline1', 'isline2', 'isline3'] : + m = RE.regExp[pattern].match(text) + if (m and m.group(1)) : + return 1 + else: + return 0 + +LINE_PREPROCESSOR = LinePreprocessor() + + +class LineBreaksPreprocessor : + """Replaces double spaces at the end of the lines with
.""" + + def run (self, lines) : + for i in range(len(lines)) : + if (lines[i].endswith(" ") + and not RE.regExp['tabbed'].match(lines[i]) ): + lines[i] += "
" + return lines + +LINE_BREAKS_PREPROCESSOR = LineBreaksPreprocessor() + + +class HtmlBlockPreprocessor : + """Removes html blocks from self.lines""" + + def run (self, lines) : + new_blocks = [] + text = "\n".join(lines) + for block in text.split("\n\n") : + if block.startswith("\n") : + block = block[1:] + if ( (block.startswith("<") and block.rstrip().endswith(">")) + and (block[1] in ["!", "?", "@", "%"] + or is_block_level( block[1:].replace(">", " ") + .split()[0].lower()))) : + new_blocks.append( + self.stash.store(block.strip())) + else : + new_blocks.append(block) + return "\n\n".join(new_blocks).split("\n") + +HTML_BLOCK_PREPROCESSOR = HtmlBlockPreprocessor() + + +class ReferencePreprocessor : + + def run (self, lines) : + new_text = []; + for line in lines: + m = RE.regExp['reference-def'].match(line) + if m: + id = m.group(2).strip().lower() + title = dequote(m.group(4).strip()) #.replace('"', """) + self.references[id] = (m.group(3), title) + else: + new_text.append(line) + return new_text #+ "\n" + +REFERENCE_PREPROCESSOR = ReferencePreprocessor() + +""" +====================================================================== +========================== INLINE PATTERNS =========================== +====================================================================== + +Inline patterns such as *emphasis* are handled by means of auxiliary +objects, one per pattern. Each pattern object uses a single regular +expression and needs support the following methods: + + pattern.getCompiledRegExp() - returns a regular expression + + pattern.handleMatch(m, doc) - takes a match object and returns + a NanoDom node (as a part of the provided + doc) or None + +All of python markdown's built-in patterns subclass from BasePatter, +but you can add additional patterns that don't. + +Also note that all the regular expressions used by inline must +capture the whole block. For this reason, they all start with +'^(.*)' and end with '(.*)!'. In case with built-in expression +BasePattern takes care of adding the "^(.*)" and "(.*)!". + +Finally, the order in which regular expressions are applied is very +important - e.g. if we first replace http://.../ links with tags +and _then_ try to replace inline html, we would end up with a mess. +So, we apply the expressions in the following order: + + * escape and backticks have to go before everything else, so + that we can preempt any markdown patterns by escaping them. + + * then we handle auto-links (must be done before inline html) + + * then we handle inline HTML. At this point we will simply + replace all inline HTML strings with a placeholder and add + the actual HTML to a hash. + + * then inline images (must be done before links) + + * then bracketed links, first regular then reference-style + + * finally we apply strong and emphasis +""" + +NOBRACKET = r'[^\]\[]*' +BRK = ( r'\[(' + + (NOBRACKET + r'(\['+NOBRACKET)*6 + + (NOBRACKET+ r'\])*'+NOBRACKET)*6 + + NOBRACKET + r')\]' ) + +BACKTICK_RE = r'\`([^\`]*)\`' # `e= m*c^2` +DOUBLE_BACKTICK_RE = r'\`\`(.*)\`\`' # ``e=f("`")`` +ESCAPE_RE = r'\\(.)' # \< +EMPHASIS_RE = r'\*([^\*]*)\*' # *emphasis* +STRONG_RE = r'\*\*(.*)\*\*' # **strong** +STRONG_EM_RE = r'\*\*\*([^_]*)\*\*\*' # ***strong*** + +if SMART_EMPHASIS: + EMPHASIS_2_RE = r'(?\)' # [text]() +IMAGE_LINK_RE = r'\!' + BRK + r'\s*\(([^\)]*)\)' # ![alttxt](http://x.com/) +REFERENCE_RE = BRK+ r'\s*\[([^\]]*)\]' # [Google][3] +IMAGE_REFERENCE_RE = r'\!' + BRK + '\s*\[([^\]]*)\]' # ![alt text][2] +NOT_STRONG_RE = r'( \* )' # stand-alone * or _ +AUTOLINK_RE = r'<(http://[^>]*)>' # +AUTOMAIL_RE = r'<([^> ]*@[^> ]*)>' # +HTML_RE = r'(\<[^\>]*\>)' # <...> +ENTITY_RE = r'(&[\#a-zA-Z0-9]*;)' # & + +class BasePattern: + + def __init__ (self, pattern) : + self.pattern = pattern + self.compiled_re = re.compile("^(.*)%s(.*)$" % pattern, re.DOTALL) + + def getCompiledRegExp (self) : + return self.compiled_re + +class SimpleTextPattern (BasePattern) : + + def handleMatch(self, m, doc) : + return doc.createTextNode(m.group(2)) + +class SimpleTagPattern (BasePattern): + + def __init__ (self, pattern, tag) : + BasePattern.__init__(self, pattern) + self.tag = tag + + def handleMatch(self, m, doc) : + el = doc.createElement(self.tag) + el.appendChild(doc.createTextNode(m.group(2))) + return el + +class BacktickPattern (BasePattern): + + def __init__ (self, pattern): + BasePattern.__init__(self, pattern) + self.tag = "code" + + def handleMatch(self, m, doc) : + el = doc.createElement(self.tag) + text = m.group(2).strip() + text = text.replace("&", "&") + el.appendChild(doc.createTextNode(text)) + return el + + +class DoubleTagPattern (SimpleTagPattern) : + + def handleMatch(self, m, doc) : + tag1, tag2 = self.tag.split(",") + el1 = doc.createElement(tag1) + el2 = doc.createElement(tag2) + el1.appendChild(el2) + el2.appendChild(doc.createTextNode(m.group(2))) + return el1 + + +class HtmlPattern (BasePattern): + + def handleMatch (self, m, doc) : + place_holder = self.stash.store(m.group(2)) + return doc.createTextNode(place_holder) + + +class LinkPattern (BasePattern): + + def handleMatch(self, m, doc) : + el = doc.createElement('a') + el.appendChild(doc.createTextNode(m.group(2))) + parts = m.group(9).split() + # We should now have [], [href], or [href, title] + if parts : + el.setAttribute('href', parts[0]) + else : + el.setAttribute('href', "") + if len(parts) > 1 : + # we also got a title + title = " ".join(parts[1:]).strip() + title = dequote(title) #.replace('"', """) + el.setAttribute('title', title) + return el + + +class ImagePattern (BasePattern): + + def handleMatch(self, m, doc): + el = doc.createElement('img') + src_parts = m.group(9).split() + el.setAttribute('src', src_parts[0]) + if len(src_parts) > 1 : + el.setAttribute('title', dequote(" ".join(src_parts[1:]))) + if ENABLE_ATTRIBUTES : + text = doc.createTextNode(m.group(2)) + el.appendChild(text) + text.handleAttributes() + truealt = text.value + el.childNodes.remove(text) + else: + truealt = m.group(2) + el.setAttribute('alt', truealt) + return el + +class ReferencePattern (BasePattern): + + def handleMatch(self, m, doc): + if m.group(9) : + id = m.group(9).lower() + else : + # if we got something like "[Google][]" + # we'll use "google" as the id + id = m.group(2).lower() + if not self.references.has_key(id) : # ignore undefined refs + return None + href, title = self.references[id] + text = m.group(2) + return self.makeTag(href, title, text, doc) + + def makeTag(self, href, title, text, doc): + el = doc.createElement('a') + el.setAttribute('href', href) + if title : + el.setAttribute('title', title) + el.appendChild(doc.createTextNode(text)) + return el + + +class ImageReferencePattern (ReferencePattern): + + def makeTag(self, href, title, text, doc): + el = doc.createElement('img') + el.setAttribute('src', href) + if title : + el.setAttribute('title', title) + el.setAttribute('alt', text) + return el + + +class AutolinkPattern (BasePattern): + + def handleMatch(self, m, doc): + el = doc.createElement('a') + el.setAttribute('href', m.group(2)) + el.appendChild(doc.createTextNode(m.group(2))) + return el + +class AutomailPattern (BasePattern): + + def handleMatch(self, m, doc) : + el = doc.createElement('a') + email = m.group(2) + if email.startswith("mailto:"): + email = email[len("mailto:"):] + for letter in email: + entity = doc.createEntityReference("#%d" % ord(letter)) + el.appendChild(entity) + mailto = "mailto:" + email + mailto = "".join(['&#%d;' % ord(letter) for letter in mailto]) + el.setAttribute('href', mailto) + return el + +ESCAPE_PATTERN = SimpleTextPattern(ESCAPE_RE) +NOT_STRONG_PATTERN = SimpleTextPattern(NOT_STRONG_RE) + +BACKTICK_PATTERN = BacktickPattern(BACKTICK_RE) +DOUBLE_BACKTICK_PATTERN = BacktickPattern(DOUBLE_BACKTICK_RE) +STRONG_PATTERN = SimpleTagPattern(STRONG_RE, 'strong') +STRONG_PATTERN_2 = SimpleTagPattern(STRONG_2_RE, 'strong') +EMPHASIS_PATTERN = SimpleTagPattern(EMPHASIS_RE, 'em') +EMPHASIS_PATTERN_2 = SimpleTagPattern(EMPHASIS_2_RE, 'em') + +STRONG_EM_PATTERN = DoubleTagPattern(STRONG_EM_RE, 'strong,em') +STRONG_EM_PATTERN_2 = DoubleTagPattern(STRONG_EM_2_RE, 'strong,em') + +LINK_PATTERN = LinkPattern(LINK_RE) +LINK_ANGLED_PATTERN = LinkPattern(LINK_ANGLED_RE) +IMAGE_LINK_PATTERN = ImagePattern(IMAGE_LINK_RE) +IMAGE_REFERENCE_PATTERN = ImageReferencePattern(IMAGE_REFERENCE_RE) +REFERENCE_PATTERN = ReferencePattern(REFERENCE_RE) + +HTML_PATTERN = HtmlPattern(HTML_RE) +ENTITY_PATTERN = HtmlPattern(ENTITY_RE) + +AUTOLINK_PATTERN = AutolinkPattern(AUTOLINK_RE) +AUTOMAIL_PATTERN = AutomailPattern(AUTOMAIL_RE) + + +""" +====================================================================== +========================== POST-PROCESSORS =========================== +====================================================================== + +Markdown also allows post-processors, which are similar to +preprocessors in that they need to implement a "run" method. Unlike +pre-processors, they take a NanoDom document as a parameter and work +with that. +# +There are currently no standard post-processors, but the footnote +extension below uses one. +""" +""" +====================================================================== +========================== MISC AUXILIARY CLASSES ==================== +====================================================================== +""" + +class HtmlStash : + """This class is used for stashing HTML objects that we extract + in the beginning and replace with place-holders.""" + + def __init__ (self) : + self.html_counter = 0 # for counting inline html segments + self.rawHtmlBlocks=[] + + def store(self, html) : + """Saves an HTML segment for later reinsertion. Returns a + placeholder string that needs to be inserted into the + document. + + @param html: an html segment + @returns : a placeholder string """ + self.rawHtmlBlocks.append(html) + placeholder = HTML_PLACEHOLDER % self.html_counter + self.html_counter += 1 + return placeholder + + +class BlockGuru : + + def _findHead(self, lines, fn, allowBlank=0) : + + """Functional magic to help determine boundaries of indented + blocks. + + @param lines: an array of strings + @param fn: a function that returns a substring of a string + if the string matches the necessary criteria + @param allowBlank: specifies whether it's ok to have blank + lines between matching functions + @returns: a list of post processes items and the unused + remainder of the original list""" + + items = [] + item = -1 + + i = 0 # to keep track of where we are + + for line in lines : + + if not line.strip() and not allowBlank: + return items, lines[i:] + + if not line.strip() and allowBlank: + # If we see a blank line, this _might_ be the end + i += 1 + + # Find the next non-blank line + for j in range(i, len(lines)) : + if lines[j].strip() : + next = lines[j] + break + else : + # There is no more text => this is the end + break + + # Check if the next non-blank line is still a part of the list + + part = fn(next) + + if part : + items.append("") + continue + else : + break # found end of the list + + part = fn(line) + + if part : + items.append(part) + i += 1 + continue + else : + return items, lines[i:] + else : + i += 1 + + return items, lines[i:] + + + def detabbed_fn(self, line) : + """ An auxiliary method to be passed to _findHead """ + m = RE.regExp['tabbed'].match(line) + if m: + return m.group(4) + else : + return None + + + def detectTabbed(self, lines) : + + return self._findHead(lines, self.detabbed_fn, + allowBlank = 1) + + +def print_error(string): + """Print an error string to stderr""" + sys.stderr.write(string +'\n') + + +def dequote(string) : + """ Removes quotes from around a string """ + if ( ( string.startswith('"') and string.endswith('"')) + or (string.startswith("'") and string.endswith("'")) ) : + return string[1:-1] + else : + return string + +""" +====================================================================== +========================== CORE MARKDOWN ============================= +====================================================================== + +This stuff is ugly, so if you are thinking of extending the syntax, +see first if you can do it via pre-processors, post-processors, +inline patterns or a combination of the three. +""" + +class CorePatterns : + """This class is scheduled for removal as part of a refactoring + effort.""" + + patterns = { + 'header': r'(#*)([^#]*)(#*)', # # A title + 'reference-def' : r'(\ ?\ ?\ ?)\[([^\]]*)\]:\s*([^ ]*)(.*)', + # [Google]: http://www.google.com/ + 'containsline': r'([-]*)$|^([=]*)', # -----, =====, etc. + 'ol': r'[ ]{0,3}[\d]*\.\s+(.*)', # 1. text + 'ul': r'[ ]{0,3}[*+-]\s+(.*)', # "* text" + 'isline1': r'(\**)', # *** + 'isline2': r'(\-*)', # --- + 'isline3': r'(\_*)', # ___ + 'tabbed': r'((\t)|( ))(.*)', # an indented line + 'quoted' : r'> ?(.*)', # a quoted block ("> ...") + } + + def __init__ (self) : + + self.regExp = {} + for key in self.patterns.keys() : + self.regExp[key] = re.compile("^%s$" % self.patterns[key], + re.DOTALL) + + self.regExp['containsline'] = re.compile(r'^([-]*)$|^([=]*)$', re.M) + +RE = CorePatterns() + + +class Markdown: + """ Markdown formatter class for creating an html document from + Markdown text """ + + + def __init__(self, source=None): + """Creates a new Markdown instance. + + @param source: The text in Markdown format. """ + + if isinstance(source, unicode): + source = source.encode('utf8') + self.source = source + self.blockGuru = BlockGuru() + self.registeredExtensions = [] + self.stripTopLevelTags = 1 + + self.preprocessors = [ HEADER_PREPROCESSOR, + LINE_PREPROCESSOR, + HTML_BLOCK_PREPROCESSOR, + LINE_BREAKS_PREPROCESSOR, + # A footnote preprocessor will + # get inserted here + REFERENCE_PREPROCESSOR ] + + + self.postprocessors = [] # a footnote postprocessor will get + # inserted later + + self.prePatterns = [] + + + self.inlinePatterns = [ DOUBLE_BACKTICK_PATTERN, + BACKTICK_PATTERN, + ESCAPE_PATTERN, + IMAGE_LINK_PATTERN, + IMAGE_REFERENCE_PATTERN, + REFERENCE_PATTERN, + LINK_ANGLED_PATTERN, + LINK_PATTERN, + AUTOLINK_PATTERN, + AUTOMAIL_PATTERN, + HTML_PATTERN, + ENTITY_PATTERN, + NOT_STRONG_PATTERN, + STRONG_EM_PATTERN, + STRONG_EM_PATTERN_2, + STRONG_PATTERN, + STRONG_PATTERN_2, + EMPHASIS_PATTERN, + EMPHASIS_PATTERN_2 + # The order of the handlers matters!!! + ] + + self.reset() + + def registerExtension(self, extension) : + self.registeredExtensions.append(extension) + + def reset(self) : + """Resets all state variables so that we can start + with a new text.""" + self.references={} + self.htmlStash = HtmlStash() + + HTML_BLOCK_PREPROCESSOR.stash = self.htmlStash + REFERENCE_PREPROCESSOR.references = self.references + HTML_PATTERN.stash = self.htmlStash + ENTITY_PATTERN.stash = self.htmlStash + REFERENCE_PATTERN.references = self.references + IMAGE_REFERENCE_PATTERN.references = self.references + + for extension in self.registeredExtensions : + extension.reset() + + + def _transform(self): + """Transforms the Markdown text into a XHTML body document + + @returns: A NanoDom Document """ + + # Setup the document + + self.doc = Document() + self.top_element = self.doc.createElement("span") + self.top_element.appendChild(self.doc.createTextNode('\n')) + self.top_element.setAttribute('class', 'markdown') + self.doc.appendChild(self.top_element) + + # Fixup the source text + text = self.source.strip() + text = text.replace("\r\n", "\n").replace("\r", "\n") + text += "\n\n" + text = text.expandtabs(TAB_LENGTH) + + # Split into lines and run the preprocessors that will work with + # self.lines + + self.lines = text.split("\n") + + # Run the pre-processors on the lines + for prep in self.preprocessors : + self.lines = prep.run(self.lines) + + # Create a NanoDom tree from the lines and attach it to Document + + + buffer = [] + for line in self.lines : + if line.startswith("#") : + self._processSection(self.top_element, buffer) + buffer = [line] + else : + buffer.append(line) + self._processSection(self.top_element, buffer) + + #self._processSection(self.top_element, self.lines) + + # Not sure why I put this in but let's leave it for now. + self.top_element.appendChild(self.doc.createTextNode('\n')) + + # Run the post-processors + for postprocessor in self.postprocessors : + postprocessor.run(self.doc) + + return self.doc + + + def _processSection(self, parent_elem, lines, + inList = 0, looseList = 0) : + + """Process a section of a source document, looking for high + level structural elements like lists, block quotes, code + segments, html blocks, etc. Some those then get stripped + of their high level markup (e.g. get unindented) and the + lower-level markup is processed recursively. + + @param parent_elem: A NanoDom element to which the content + will be added + @param lines: a list of lines + @param inList: a level + @returns: None""" + + if not lines : + return + + # Check if this section starts with a list, a blockquote or + # a code block + + processFn = { 'ul' : self._processUList, + 'ol' : self._processOList, + 'quoted' : self._processQuote, + 'tabbed' : self._processCodeBlock } + + for regexp in ['ul', 'ol', 'quoted', 'tabbed'] : + m = RE.regExp[regexp].match(lines[0]) + if m : + processFn[regexp](parent_elem, lines, inList) + return + + # We are NOT looking at one of the high-level structures like + # lists or blockquotes. So, it's just a regular paragraph + # (though perhaps nested inside a list or something else). If + # we are NOT inside a list, we just need to look for a blank + # line to find the end of the block. If we ARE inside a + # list, however, we need to consider that a sublist does not + # need to be separated by a blank line. Rather, the following + # markup is legal: + # + # * The top level list item + # + # Another paragraph of the list. This is where we are now. + # * Underneath we might have a sublist. + # + + if inList : + + start, theRest = self._linesUntil(lines, (lambda line: + RE.regExp['ul'].match(line) + or RE.regExp['ol'].match(line) + or not line.strip())) + + self._processSection(parent_elem, start, + inList - 1, looseList = looseList) + self._processSection(parent_elem, theRest, + inList - 1, looseList = looseList) + + + else : # Ok, so it's just a simple block + + paragraph, theRest = self._linesUntil(lines, lambda line: + not line.strip()) + + if len(paragraph) and paragraph[0].startswith('#') : + m = RE.regExp['header'].match(paragraph[0]) + if m : + level = len(m.group(1)) + h = self.doc.createElement("h%d" % level) + parent_elem.appendChild(h) + for item in self._handleInlineWrapper2(m.group(2).strip()) : + h.appendChild(item) + else : + message(CRITICAL, "We've got a problem header!") + + elif paragraph : + + list = self._handleInlineWrapper2("\n".join(paragraph)) + + if ( parent_elem.nodeName == 'li' + and not (looseList or parent_elem.childNodes)): + + #and not parent_elem.childNodes) : + # If this is the first paragraph inside "li", don't + # put

around it - append the paragraph bits directly + # onto parent_elem + el = parent_elem + else : + # Otherwise make a "p" element + el = self.doc.createElement("p") + parent_elem.appendChild(el) + + for item in list : + el.appendChild(item) + + if theRest : + theRest = theRest[1:] # skip the first (blank) line + + self._processSection(parent_elem, theRest, inList) + + + + def _processUList(self, parent_elem, lines, inList) : + self._processList(parent_elem, lines, inList, + listexpr='ul', tag = 'ul') + + def _processOList(self, parent_elem, lines, inList) : + self._processList(parent_elem, lines, inList, + listexpr='ol', tag = 'ol') + + + def _processList(self, parent_elem, lines, inList, listexpr, tag) : + """Given a list of document lines starting with a list item, + finds the end of the list, breaks it up, and recursively + processes each list item and the remainder of the text file. + + @param parent_elem: A dom element to which the content will be added + @param lines: a list of lines + @param inList: a level + @returns: None""" + + ul = self.doc.createElement(tag) # ul might actually be '

    ' + parent_elem.appendChild(ul) + + looseList = 0 + + # Make a list of list items + items = [] + item = -1 + + i = 0 # a counter to keep track of where we are + + for line in lines : + + loose = 0 + if not line.strip() : + # If we see a blank line, this _might_ be the end of the list + i += 1 + loose = 1 + + # Find the next non-blank line + for j in range(i, len(lines)) : + if lines[j].strip() : + next = lines[j] + break + else : + # There is no more text => end of the list + break + + # Check if the next non-blank line is still a part of the list + if ( RE.regExp['ul'].match(next) or + RE.regExp['ol'].match(next) or + RE.regExp['tabbed'].match(next) ): + # get rid of any white space in the line + items[item].append(line.strip()) + looseList = loose or looseList + continue + else : + break # found end of the list + + # Now we need to detect list items (at the current level) + # while also detabing child elements if necessary + + for expr in ['ul', 'ol', 'tabbed']: + + m = RE.regExp[expr].match(line) + if m : + if expr in ['ul', 'ol'] : # We are looking at a new item + if m.group(1) : + items.append([m.group(1)]) + item += 1 + elif expr == 'tabbed' : # This line needs to be detabbed + items[item].append(m.group(4)) #after the 'tab' + + i += 1 + break + else : + items[item].append(line) # Just regular continuation + i += 1 # added on 2006.02.25 + else : + i += 1 + + # Add the dom elements + for item in items : + li = self.doc.createElement("li") + ul.appendChild(li) + + self._processSection(li, item, inList + 1, looseList = looseList) + + # Process the remaining part of the section + + self._processSection(parent_elem, lines[i:], inList) + + + def _linesUntil(self, lines, condition) : + """ A utility function to break a list of lines upon the + first line that satisfied a condition. The condition + argument should be a predicate function. + """ + + i = -1 + for line in lines : + i += 1 + if condition(line) : break + else : + i += 1 + return lines[:i], lines[i:] + + def _processQuote(self, parent_elem, lines, inList) : + """Given a list of document lines starting with a quote finds + the end of the quote, unindents it and recursively + processes the body of the quote and the remainder of the + text file. + + @param parent_elem: DOM element to which the content will be added + @param lines: a list of lines + @param inList: a level + @returns: None """ + + dequoted = [] + i = 0 + for line in lines : + m = RE.regExp['quoted'].match(line) + if m : + dequoted.append(m.group(1)) + i += 1 + else : + break + else : + i += 1 + + blockquote = self.doc.createElement('blockquote') + parent_elem.appendChild(blockquote) + + self._processSection(blockquote, dequoted, inList) + self._processSection(parent_elem, lines[i:], inList) + + + + + def _processCodeBlock(self, parent_elem, lines, inList) : + """Given a list of document lines starting with a code block + finds the end of the block, puts it into the dom verbatim + wrapped in ("
    ") and recursively processes the
    +           the remainder of the text file.
    +
    +           @param parent_elem: DOM element to which the content will be added
    +           @param lines: a list of lines
    +           @param inList: a level
    +           @returns: None"""
    +
    +        detabbed, theRest = self.blockGuru.detectTabbed(lines)
    +
    +        pre = self.doc.createElement('pre')
    +        code = self.doc.createElement('code')
    +        parent_elem.appendChild(pre)
    +        pre.appendChild(code)
    +        text = "\n".join(detabbed).rstrip()+"\n"
    +        text = text.replace("&", "&")
    +        code.appendChild(self.doc.createTextNode(text))
    +        self._processSection(parent_elem, theRest, inList)
    +
    +
    +    def _handleInlineWrapper2 (self, line) :
    +
    +
    +        parts = [line]
    +
    +        #if not(line):
    +        #    return [self.doc.createTextNode(' ')]
    +
    +        for pattern in self.inlinePatterns :
    +
    +            #print
    +            #print self.inlinePatterns.index(pattern)
    +
    +            i = 0
    +
    +            #print parts
    +            while i < len(parts) :
    +                
    +                x = parts[i]
    +                #print i
    +                if isinstance(x, (str, unicode)) :
    +                    result = self._applyPattern(x, pattern)
    +                    #print result
    +                    #print result
    +                    #print parts, i
    +                    if result :
    +                        i -= 1
    +                        parts.remove(x)
    +                        for y in result :
    +                            parts.insert(i+1,y)
    +                
    +                i += 1
    +
    +        for i in range(len(parts)) :
    +            x = parts[i]
    +            if isinstance(x, (str, unicode)) :
    +                parts[i] = self.doc.createTextNode(x)
    +
    +        return parts
    +        
    +
    +
    +    def _handleInlineWrapper (self, line) :
    +
    +        # A wrapper around _handleInline to avoid recursion
    +
    +        parts = [line]
    +
    +        i = 0
    +        
    +        while i < len(parts) :
    +            x = parts[i]
    +            if isinstance(x, (str, unicode)) :
    +                parts.remove(x)
    +                result = self._handleInline(x)
    +                for y in result :
    +                    parts.insert(i,y)
    +            else :
    +                i += 1
    +
    +        return parts
    +
    +    def _handleInline(self,  line):
    +        """Transform a Markdown line with inline elements to an XHTML
    +        fragment.
    +
    +        This function uses auxiliary objects called inline patterns.
    +        See notes on inline patterns above.
    +
    +        @param item: A block of Markdown text
    +        @return: A list of NanoDom nodes """
    +
    +        if not(line):
    +            return [self.doc.createTextNode(' ')]
    +
    +        for pattern in self.inlinePatterns :
    +            list = self._applyPattern( line, pattern)
    +            if list: return list
    +
    +        return [self.doc.createTextNode(line)]
    +
    +    def _applyPattern(self, line, pattern) :
    +        """ Given a pattern name, this function checks if the line
    +        fits the pattern, creates the necessary elements, and returns
    +        back a list consisting of NanoDom elements and/or strings.
    +        
    +        @param line: the text to be processed
    +        @param pattern: the pattern to be checked
    +
    +        @returns: the appropriate newly created NanoDom element if the
    +                  pattern matches, None otherwise.
    +        """
    +
    +        # match the line to pattern's pre-compiled reg exp.
    +        # if no match, move on.
    +
    +        m = pattern.getCompiledRegExp().match(line)
    +        if not m :
    +            return None
    +
    +        # if we got a match let the pattern make us a NanoDom node
    +        # if it doesn't, move on
    +        node = pattern.handleMatch(m, self.doc)
    +
    +        if node :
    +            # Those are in the reverse order!
    +            return ( m.groups()[-1], # the string to the left
    +                     node,           # the new node
    +                     m.group(1))     # the string to the right of the match
    +
    +        else :
    +            return None
    +
    +    def __str__(self):
    +        """Return the document in XHTML format.
    +
    +        @returns: A serialized XHTML body."""
    +        #try :
    +        doc = self._transform()
    +        xml = doc.toxml()
    +        #finally:
    +        #    doc.unlink()
    +
    +        # Let's stick in all the raw html pieces
    +
    +        for i in range(self.htmlStash.html_counter) :
    +            xml = xml.replace("

    %s\n

    " % (HTML_PLACEHOLDER % i), + self.htmlStash.rawHtmlBlocks[i] + "\n") + xml = xml.replace(HTML_PLACEHOLDER % i, + self.htmlStash.rawHtmlBlocks[i]) + + xml = xml.replace(FN_BACKLINK_TEXT, "↩") + + # And return everything but the top level tag + + if self.stripTopLevelTags : + xml = xml.strip()[23:-7] + + if isinstance(xml, unicode) : + xml = xml.encode("utf8") + + return xml + + + toString = __str__ + + +""" +========================= FOOTNOTES ================================= + +This section adds footnote handling to markdown. It can be used as +an example for extending python-markdown with relatively complex +functionality. While in this case the extension is included inside +the module itself, it could just as easily be added from outside the +module. Not that all markdown classes above are ignorant about +footnotes. All footnote functionality is provided separately and +then added to the markdown instance at the run time. + +Footnote functionality is attached by calling extendMarkdown() +method of FootnoteExtension. The method also registers the +extension to allow it's state to be reset by a call to reset() +method. +""" + +class FootnoteExtension : + + DEF_RE = re.compile(r'(\ ?\ ?\ ?)\[\^([^\]]*)\]:\s*(.*)') + SHORT_USE_RE = re.compile(r'\[\^([^\]]*)\]', re.M) # [^a] + + FN_PLACE_MARKER = "///Footnotes Go Here///" + + def __init__ (self) : + self.reset() + + def extendMarkdown(self, md) : + + self.md = md + + # Stateless extensions do not need to be registered + md.registerExtension(self) + + # Insert a preprocessor before ReferencePreprocessor + index = md.preprocessors.index(REFERENCE_PREPROCESSOR) + preprocessor = FootnotePreprocessor(self) + preprocessor.md = md + md.preprocessors.insert(index, preprocessor) + + # Insert an inline pattern before ImageReferencePattern + FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah + index = md.inlinePatterns.index(IMAGE_REFERENCE_PATTERN) + md.inlinePatterns.insert(index, FootnotePattern(FOOTNOTE_RE, self)) + + # Insert a post-processor that would actually add the footnote div + postprocessor = FootnotePostprocessor(self) + postprocessor.extension = self + + md.postprocessors.append(postprocessor) + + + def reset(self) : + # May be called by Markdown is state reset is desired + + self.footnote_suffix = "-" + str(int(random.random()*1000000000)) + self.used_footnotes={} + self.footnotes = {} + + def findFootnotesPlaceholder(self, doc) : + def findFootnotePlaceholderFn(node=None, indent=0): + if node.type == 'text': + if node.value.find(self.FN_PLACE_MARKER) > -1 : + return True + + fn_div_list = doc.find(findFootnotePlaceholderFn) + if fn_div_list : + return fn_div_list[0] + + + def setFootnote(self, id, text) : + self.footnotes[id] = text + + def makeFootnoteId(self, num) : + return 'fn%d%s' % (num, self.footnote_suffix) + + def makeFootnoteRefId(self, num) : + return 'fnr%d%s' % (num, self.footnote_suffix) + + def makeFootnotesDiv (self, doc) : + """Creates the div with class='footnote' and populates it with + the text of the footnotes. + + @returns: the footnote div as a dom element """ + + if not self.footnotes.keys() : + return None + + div = doc.createElement("div") + div.setAttribute('class', 'footnote') + hr = doc.createElement("hr") + div.appendChild(hr) + ol = doc.createElement("ol") + div.appendChild(ol) + + footnotes = [(self.used_footnotes[id], id) + for id in self.footnotes.keys()] + footnotes.sort() + + for i, id in footnotes : + li = doc.createElement('li') + li.setAttribute('id', self.makeFootnoteId(i)) + + self.md._processSection(li, self.footnotes[id].split("\n")) + + #li.appendChild(doc.createTextNode(self.footnotes[id])) + + backlink = doc.createElement('a') + backlink.setAttribute('href', '#' + self.makeFootnoteRefId(i)) + backlink.setAttribute('class', 'footnoteBackLink') + backlink.setAttribute('title', + 'Jump back to footnote %d in the text' % 1) + backlink.appendChild(doc.createTextNode(FN_BACKLINK_TEXT)) + + if li.childNodes : + node = li.childNodes[-1] + if node.type == "text" : + node = li + node.appendChild(backlink) + + ol.appendChild(li) + + return div + + +class FootnotePreprocessor : + + def __init__ (self, footnotes) : + self.footnotes = footnotes + + def run(self, lines) : + + self.blockGuru = BlockGuru() + lines = self._handleFootnoteDefinitions (lines) + + # Make a hash of all footnote marks in the text so that we + # know in what order they are supposed to appear. (This + # function call doesn't really substitute anything - it's just + # a way to get a callback for each occurence. + + text = "\n".join(lines) + self.footnotes.SHORT_USE_RE.sub(self.recordFootnoteUse, text) + + return text.split("\n") + + + def recordFootnoteUse(self, match) : + + id = match.group(1) + id = id.strip() + nextNum = len(self.footnotes.used_footnotes.keys()) + 1 + self.footnotes.used_footnotes[id] = nextNum + + + def _handleFootnoteDefinitions(self, lines) : + """Recursively finds all footnote definitions in the lines. + + @param lines: a list of lines of text + @returns: a string representing the text with footnote + definitions removed """ + + i, id, footnote = self._findFootnoteDefinition(lines) + + if id : + + plain = lines[:i] + + detabbed, theRest = self.blockGuru.detectTabbed(lines[i+1:]) + + self.footnotes.setFootnote(id, + footnote + "\n" + + "\n".join(detabbed)) + + more_plain = self._handleFootnoteDefinitions(theRest) + return plain + [""] + more_plain + + else : + return lines + + def _findFootnoteDefinition(self, lines) : + """Finds the first line of a footnote definition. + + @param lines: a list of lines of text + @returns: the index of the line containing a footnote definition """ + + counter = 0 + for line in lines : + m = self.footnotes.DEF_RE.match(line) + if m : + return counter, m.group(2), m.group(3) + counter += 1 + return counter, None, None + + +class FootnotePattern (BasePattern) : + + def __init__ (self, pattern, footnotes) : + + BasePattern.__init__(self, pattern) + self.footnotes = footnotes + + def handleMatch(self, m, doc) : + sup = doc.createElement('sup') + a = doc.createElement('a') + sup.appendChild(a) + id = m.group(2) + num = self.footnotes.used_footnotes[id] + sup.setAttribute('id', self.footnotes.makeFootnoteRefId(num)) + a.setAttribute('href', '#' + self.footnotes.makeFootnoteId(num)) + a.appendChild(doc.createTextNode(str(num))) + return sup + +class FootnotePostprocessor : + + def __init__ (self, footnotes) : + self.footnotes = footnotes + + def run(self, doc) : + footnotesDiv = self.footnotes.makeFootnotesDiv(doc) + if footnotesDiv : + fnPlaceholder = self.extension.findFootnotesPlaceholder(doc) + if fnPlaceholder : + fnPlaceholder.parent.replaceChild(fnPlaceholder, footnotesDiv) + else : + doc.documentElement.appendChild(footnotesDiv) + +# ==================================================================== + +def markdown(text) : + message(VERBOSE, "in markdown.py, received text:\n%s" % text) + return Markdown(text).toString() + +def markdownWithFootnotes(text): + message(VERBOSE, "Running markdown with footnotes, " + + "received text:\n%s" % text) + md = Markdown() + footnoteExtension = FootnoteExtension() + footnoteExtension.extendMarkdown(md) + md.source = text + + return str(md) + +def test_markdown(args): + """test markdown at the command line. + in each test, arg 0 is the module name""" + print "\nTEST 1: no arguments on command line" + cmd_line(["markdown.py"]) + print "\nTEST 2a: 1 argument on command line: a good option" + cmd_line(["markdown.py","-footnotes"]) + print "\nTEST 2b: 1 argument on command line: a bad option" + cmd_line(["markdown.py","-foodnotes"]) + print "\nTEST 3: 1 argument on command line: non-existent input file" + cmd_line(["markdown.py","junk.txt"]) + print "\nTEST 4: 1 argument on command line: existing input file" + lines = """ +Markdown text with[^1]: + +2. **bold text**, +3. *italic text*. + +Then more: + + beginning of code block; + another line of code block. + + a second paragraph of code block. + +more text to end our file. + +[^1]: "italic" means emphasis. +""" + fid = "markdown-test.txt" + f1 = open(fid, 'w+') + f1.write(lines) + f1.close() + cmd_line(["markdown.py",fid]) + print "\nTEST 5: 2 arguments on command line: nofootnotes and input file" + cmd_line(["markdown.py","-nofootnotes", fid]) + print "\nTEST 6: 2 arguments on command line: footnotes and input file" + cmd_line(["markdown.py","-footnotes", fid]) + print "\nTEST 7: 3 arguments on command line: nofootnotes,inputfile, outputfile" + fidout = "markdown-test.html" + cmd_line(["markdown.py","-nofootnotes", fid, fidout]) + + +def get_vars(args): + """process the command-line args received; return usable variables""" + #firstly get the variables + + message(VERBOSE, "in get_vars(), args: %s" % args) + + if len(args) <= 1: + option, inFile, outFile = (None, None, None) + elif len(args) >= 4: + option, inFile, outFile = args[1:4] + elif len(args) == 3: + temp1, temp2 = args[1:3] + if temp1[0] == '-': + #then we have an option and inFile + option, inFile, outFile = temp1, temp2, None + else: + #we have no option, so we must have inFile and outFile + option, inFile, outFile = None, temp1, temp2 + else: + #len(args) = 2 + #we have only one usable arg: might be an option or a file + temp1 = args[1] + + message(VERBOSE, "our single arg is: %s" % str(temp1)) + + if temp1[0] == '-': + #then we have an option + option, inFile, outFile = temp1, None, None + else: + #we have no option, so we must have inFile + option, inFile, outFile = None, temp1, None + + message(VERBOSE, + "prior to validation, option: %s, inFile: %s, outFile: %s" % + (str(option), str(inFile), str(outFile),)) + + return option, inFile, outFile + + +USAGE = """ +\nUsing markdown.py: + + python markdown.py [option] input_file_with_markdown.txt [output_file.html] + +Options: + + -footnotes or -fn : generate markdown with footnotes + -test or -t : run a self-test + -help or -h : print this message + +""" + +VALID_OPTIONS = ['footnotes','nofootnotes', 'fn', 'test', 't', 'f', + 'help', 'h'] + +EXPANDED_OPTIONS = { "fn" : "footnotes", + "t" : "test", + "h" : "help" } + + +def validate_option(option) : + + """ Check if the option makes sense and print an appropriate message + if it isn't. + + @return: valid option string or None + """ + + #now validate the variables + if (option is not None): + if (len(option) > 1 and option[1:] in VALID_OPTIONS) : + option = option[1:] + + if option in EXPANDED_OPTIONS.keys() : + option = EXPANDED_OPTIONS[option] + return option + else: + message(CRITICAL, + "\nSorry, I don't understand option %s" % option) + message(CRITICAL, USAGE) + return None + + +def validate_input_file(inFile) : + """ Check if the input file is specified and exists. + + @return: valid input file path or None + """ + + if not inFile : + message(CRITICAL, + "\nI need an input filename.\n") + message(CRITICAL, USAGE) + return None + + + if os.access(inFile, os.R_OK): + return inFile + else : + message(CRITICAL, "Sorry, I can't find input file %s" % str(inFile)) + return None + + + + +def cmd_line(args): + + message(VERBOSE, "in cmd_line with args: %s" % args) + + option, inFile, outFile = get_vars(args) + + if option : + option = validate_option(option) + if not option : return + + if option == "help" : + message(CRITICAL, USAGE) + return + elif option == "test" : + test_markdown(None) + return + + inFile = validate_input_file(inFile) + if not inFile : + return + else : + input = file(inFile).read() + + message(VERBOSE, "Validated command line parameters:" + + "\n\toption: %s, \n\tinFile: %s, \n\toutFile: %s" % ( + str(option), str(inFile), str(outFile),)) + + if option == "footnotes" : + md_function = markdownWithFootnotes + else : + md_function = markdown + + if outFile is None: + print md_function(input) + else: + output = md_function(input) + f1 = open(outFile, "w+") + f1.write(output) + f1.close() + + if os.access(outFile, os.F_OK): + message(INFO, "Successfully wrote %s" % outFile) + else: + message(INFO, "Failed to write %s" % outFile) + + +if __name__ == '__main__': + """ Run Markdown from the command line. + Set debug = 3 at top of file to get diagnostic output""" + args = sys.argv + + #set testing=1 to test the command-line response of markdown.py + testing = 0 + if testing: + test_markdown(args) + else: + import time + t0 = time.time() + #for x in range(10) : + cmd_line(args) + #import profile + #profile.run('cmd_line(args)', 'profile') + t1 = time.time() + #print "Time: %f - %f = %f" % (t1, t0, t1-t0) + +""" +CHANGELOG +========= + +May 15, 2006: A bug with lists, recursion on block-level elements, +run-in headers, spaces before headers, unicode input (thanks to Aaron +Swartz). Sourceforge tracker #s: 1489313, 1489312, 1489311, 1488370, +1485178, 1485176. (v. 1.5) + +Mar. 24, 2006: Switched to a not-so-recursive algorithm with +_handleInline. (Version 1.4) + +Mar. 15, 2006: Replaced some instance variables with class variables +(a patch from Stelios Xanthakis). Chris Clark's new regexps that do +not trigger midword underlining. + +Feb. 28, 2006: Clean-up and command-line handling by Stewart +Midwinter. (Version 1.3) + +Feb. 24, 2006: Fixed a bug with the last line of the list appearing +again as a separate paragraph. Incorporated Chris Clark's "mailto" +patch. Added support for
    at the end of lines ending in two or +more spaces. Fixed a crashing bug when using ImageReferencePattern. +Added several utility methods to Nanodom. (Version 1.2) + +Jan. 31, 2006: Added "hr" and "hr/" to BLOCK_LEVEL_ELEMENTS and +changed
    to
    . (Thanks to Sergej Chodarev.) + +Nov. 26, 2005: Fixed a bug with certain tabbed lines inside lists +getting wrapped in
    .  (v. 1.1)
    +
    +Nov. 19, 2005: Made "a {
    +	text-decoration: none;
    +	color: #46494d;
    +}
    +.results td {
    +	padding-right: 5px;
    +	vertical-align: top;
    +	font-size: 0.8em;
    +}
    +.listing th {
    +	background: #d1d3d6;
    +	border-left: 1px solid #387cbf;
    +	font-size: small;
    +	vertical-align: top;
    +	text-align: left;
    +	padding: 2px;
    +}
    +.listing td {
    +	font-size: small;
    +	padding: 2px;
    +}
    +blockquote.code {
    +	background: #c1c3f6;
    +	border: 1px solid #8faecd;
    +	margin-left: auto;
    +	margin-right: auto;
    +	white-space: nowrap;
    +	padding: 5px;
    +	font-family: Courier, Courier New, Monospace;
    +}
    +/*
    + * Wiki Styles
    + */
    +h1.wiki {
    +	border-bottom: 1px solid #46494d;
    +}
    +div.wikifoot_l {
    +	font-size: x-small;
    +	text-align: left;
    +	padding-top: 25px;
    +}
    +div.wikifoot_r {
    +	font-size: x-small;
    +	text-align: right;
    +	float: right;
    +	padding-top: 25px;
    +}
    +.wikibody {
    +	padding-top: 15px;
    +}
    +.wikibody ol {
    +	padding-left: 28px;
    +	padding-top: 0px;
    +}
    +.wikibody ul {
    +	padding-left: 25px;
    +	padding-top: 0px;
    +}
    +.wikibody dd {
    +	padding-left: 30px;
    +}
    +.wikibody pre code {
    +	background: #c1c3f6;
    +	border: 1px solid #8faecd;
    +	margin-left: auto;
    +	margin-right: auto;
    +	white-space: nowrap;
    +	padding: 5px;
    +	font-family: Courier, Courier New, Monospace;
    +}
    +.wikibody blockquote {
    +	padding-left: 30px;
    +}
    +.wikibody td {
    +	padding: 5px;
    +	border: 1px solid black;
    +}
    +
    +/* Used by Django's FormWrappers */
    +textarea.vLargeTextField {
    +	width: 450px;
    +	height: 250px;
    +}
    +.pkgr2 {
    +	background-color: #eee4cb;
    +}
    +
    diff --git a/media/calendar.css b/media/calendar.css
    new file mode 100644
    index 0000000..fa5c093
    --- /dev/null
    +++ b/media/calendar.css
    @@ -0,0 +1,265 @@
    +/* The main calendar widget.  DIV containing a table. */
    +
    +.calendar {
    +  position: relative;
    +  display: none;
    +  border-top: 2px solid #fff;
    +  border-right: 2px solid #000;
    +  border-bottom: 2px solid #000;
    +  border-left: 2px solid #fff;
    +  font-size: 11px;
    +  color: #000;
    +  cursor: default;
    +  background: #c8d0d4;
    +  font-family: tahoma,verdana,sans-serif;
    +}
    +
    +.calendar table {
    +  border-top: 1px solid #000;
    +  border-right: 1px solid #fff;
    +  border-bottom: 1px solid #fff;
    +  border-left: 1px solid #000;
    +  font-size: 11px;
    +  color: #000;
    +  cursor: default;
    +  background: #c8d0d4;
    +  font-family: tahoma,verdana,sans-serif;
    +}
    +
    +/* Header part -- contains navigation buttons and day names. */
    +
    +.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */
    +  text-align: center;
    +  padding: 1px;
    +  border-top: 1px solid #fff;
    +  border-right: 1px solid #000;
    +  border-bottom: 1px solid #000;
    +  border-left: 1px solid #fff;
    +}
    +
    +.calendar .nav {
    +  background: transparent url(menuarrow.gif) no-repeat 100% 100%;
    +}
    +
    +.calendar thead .title { /* This holds the current "month, year" */
    +  font-weight: bold;
    +  padding: 1px;
    +  border: 1px solid #000;
    +  background: #788084;
    +  color: #fff;
    +  text-align: center;
    +}
    +
    +.calendar thead .headrow { /* Row  containing navigation buttons */
    +}
    +
    +.calendar thead .daynames { /* Row  containing the day names */
    +}
    +
    +.calendar thead .name { /* Cells  containing the day names */
    +  border-bottom: 1px solid #000;
    +  padding: 2px;
    +  text-align: center;
    +  background: #e8f0f4;
    +}
    +
    +.calendar thead .weekend { /* How a weekend day name shows in header */
    +  color: #f00;
    +}
    +
    +.calendar thead .hilite { /* How do the buttons in header appear when hover */
    +  border-top: 2px solid #fff;
    +  border-right: 2px solid #000;
    +  border-bottom: 2px solid #000;
    +  border-left: 2px solid #fff;
    +  padding: 0px;
    +  background-color: #d8e0e4;
    +}
    +
    +.calendar thead .active { /* Active (pressed) buttons in header */
    +  padding: 2px 0px 0px 2px;
    +  border-top: 1px solid #000;
    +  border-right: 1px solid #fff;
    +  border-bottom: 1px solid #fff;
    +  border-left: 1px solid #000;
    +  background-color: #b8c0c4;
    +}
    +
    +/* The body part -- contains all the days in month. */
    +
    +.calendar tbody .day { /* Cells  containing month days dates */
    +  width: 2em;
    +  text-align: right;
    +  padding: 2px 4px 2px 2px;
    +}
    +.calendar tbody .day.othermonth {
    +  font-size: 80%;
    +  color: #aaa;
    +}
    +.calendar tbody .day.othermonth.oweekend {
    +  color: #faa;
    +}
    +
    +.calendar table .wn {
    +  padding: 2px 3px 2px 2px;
    +  border-right: 1px solid #000;
    +  background: #e8f4f0;
    +}
    +
    +.calendar tbody .rowhilite td {
    +  background: #d8e4e0;
    +}
    +
    +.calendar tbody .rowhilite td.wn {
    +  background: #c8d4d0;
    +}
    +
    +.calendar tbody td.hilite { /* Hovered cells  */
    +  padding: 1px 3px 1px 1px;
    +  border: 1px solid;
    +  border-color: #fff #000 #000 #fff;
    +}
    +
    +.calendar tbody td.active { /* Active (pressed) cells  */
    +  padding: 2px 2px 0px 2px;
    +  border: 1px solid;
    +  border-color: #000 #fff #fff #000;
    +}
    +
    +.calendar tbody td.selected { /* Cell showing selected date */
    +  font-weight: bold;
    +  padding: 2px 2px 0px 2px;
    +  border: 1px solid;
    +  border-color: #000 #fff #fff #000;
    +  background: #d8e0e4;
    +}
    +
    +.calendar tbody td.weekend { /* Cells showing weekend days */
    +  color: #f00;
    +}
    +
    +.calendar tbody td.today { /* Cell showing today date */
    +  font-weight: bold;
    +  color: #00f;
    +}
    +
    +.calendar tbody .disabled { color: #999; }
    +
    +.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */
    +  visibility: hidden;
    +}
    +
    +.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */
    +  display: none;
    +}
    +
    +/* The footer part -- status bar and "Close" button */
    +
    +.calendar tfoot .footrow { /* The  in footer (only one right now) */
    +}
    +
    +.calendar tfoot .ttip { /* Tooltip (status bar) cell  */
    +  background: #e8f0f4;
    +  padding: 1px;
    +  border: 1px solid #000;
    +  background: #788084;
    +  color: #fff;
    +  text-align: center;
    +}
    +
    +.calendar tfoot .hilite { /* Hover style for buttons in footer */
    +  border-top: 1px solid #fff;
    +  border-right: 1px solid #000;
    +  border-bottom: 1px solid #000;
    +  border-left: 1px solid #fff;
    +  padding: 1px;
    +  background: #d8e0e4;
    +}
    +
    +.calendar tfoot .active { /* Active (pressed) style for buttons in footer */
    +  padding: 2px 0px 0px 2px;
    +  border-top: 1px solid #000;
    +  border-right: 1px solid #fff;
    +  border-bottom: 1px solid #fff;
    +  border-left: 1px solid #000;
    +}
    +
    +/* Combo boxes (menus that display months/years for direct selection) */
    +
    +.calendar .combo {
    +  position: absolute;
    +  display: none;
    +  width: 4em;
    +  top: 0px;
    +  left: 0px;
    +  cursor: default;
    +  border-top: 1px solid #fff;
    +  border-right: 1px solid #000;
    +  border-bottom: 1px solid #000;
    +  border-left: 1px solid #fff;
    +  background: #d8e0e4;
    +  font-size: 90%;
    +  padding: 1px;
    +  z-index: 100;
    +}
    +
    +.calendar .combo .label,
    +.calendar .combo .label-IEfix {
    +  text-align: center;
    +  padding: 1px;
    +}
    +
    +.calendar .combo .label-IEfix {
    +  width: 4em;
    +}
    +
    +.calendar .combo .active {
    +  background: #c8d0d4;
    +  padding: 0px;
    +  border-top: 1px solid #000;
    +  border-right: 1px solid #fff;
    +  border-bottom: 1px solid #fff;
    +  border-left: 1px solid #000;
    +}
    +
    +.calendar .combo .hilite {
    +  background: #048;
    +  color: #aef;
    +}
    +
    +.calendar td.time {
    +  border-top: 1px solid #000;
    +  padding: 1px 0px;
    +  text-align: center;
    +  background-color: #e8f0f4;
    +}
    +
    +.calendar td.time .hour,
    +.calendar td.time .minute,
    +.calendar td.time .ampm {
    +  padding: 0px 3px 0px 4px;
    +  border: 1px solid #889;
    +  font-weight: bold;
    +  background-color: #fff;
    +}
    +
    +.calendar td.time .ampm {
    +  text-align: center;
    +}
    +
    +.calendar td.time .colon {
    +  padding: 0px 2px 0px 3px;
    +  font-weight: bold;
    +}
    +
    +.calendar td.time span.hilite {
    +  border-color: #000;
    +  background-color: #667;
    +  color: #fff;
    +}
    +
    +.calendar td.time span.active {
    +  border-color: #f00;
    +  background-color: #000;
    +  color: #0f0;
    +}
    diff --git a/media/calendar.js b/media/calendar.js
    new file mode 100644
    index 0000000..c4f388d
    --- /dev/null
    +++ b/media/calendar.js
    @@ -0,0 +1,2133 @@
    +/*  Copyright Mihai Bazon, 2002-2005  |  www.bazon.net/mishoo
    + * -----------------------------------------------------------
    + *
    + * The DHTML Calendar, version 1.0 "It is happening again"
    + *
    + * Details and latest version at:
    + * www.dynarch.com/projects/calendar
    + *
    + * This script is developed by Dynarch.com.  Visit us at www.dynarch.com.
    + *
    + * This script is distributed under the GNU Lesser General Public License.
    + * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
    + */
    +
    +// $Id: calendar.js,v 1.1 2005/09/06 08:38:43 ben Exp $
    +
    +/** The Calendar object constructor. */
    +Calendar = function (firstDayOfWeek, dateStr, onSelected, onClose) {
    +	// member variables
    +	this.activeDiv = null;
    +	this.currentDateEl = null;
    +	this.getDateStatus = null;
    +	this.getDateToolTip = null;
    +	this.getDateText = null;
    +	this.timeout = null;
    +	this.onSelected = onSelected || null;
    +	this.onClose = onClose || null;
    +	this.dragging = false;
    +	this.hidden = false;
    +	this.minYear = 1970;
    +	this.maxYear = 2050;
    +	this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"];
    +	this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"];
    +	this.isPopup = true;
    +	this.weekNumbers = true;
    +	this.firstDayOfWeek = typeof firstDayOfWeek == "number" ? firstDayOfWeek : Calendar._FD; // 0 for Sunday, 1 for Monday, etc.
    +	this.showsOtherMonths = false;
    +	this.dateStr = dateStr;
    +	this.ar_days = null;
    +	this.showsTime = false;
    +	this.time24 = true;
    +	this.yearStep = 2;
    +	this.hiliteToday = true;
    +	this.multiple = null;
    +	// HTML elements
    +	this.table = null;
    +	this.element = null;
    +	this.tbody = null;
    +	this.firstdayname = null;
    +	// Combo boxes
    +	this.monthsCombo = null;
    +	this.yearsCombo = null;
    +	this.hilitedMonth = null;
    +	this.activeMonth = null;
    +	this.hilitedYear = null;
    +	this.activeYear = null;
    +	// Information
    +	this.dateClicked = false;
    +
    +	// one-time initializations
    +	if (typeof Calendar._SDN == "undefined") {
    +		// table of short day names
    +		if (typeof Calendar._SDN_len == "undefined")
    +			Calendar._SDN_len = 3;
    +		var ar = new Array();
    +		for (var i = 8; i > 0;) {
    +			ar[--i] = Calendar._DN[i].substr(0, Calendar._SDN_len);
    +		}
    +		Calendar._SDN = ar;
    +		// table of short month names
    +		if (typeof Calendar._SMN_len == "undefined")
    +			Calendar._SMN_len = 3;
    +		ar = new Array();
    +		for (var i = 12; i > 0;) {
    +			ar[--i] = Calendar._MN[i].substr(0, Calendar._SMN_len);
    +		}
    +		Calendar._SMN = ar;
    +	}
    +};
    +
    +// ** constants
    +
    +/// "static", needed for event handlers.
    +Calendar._C = null;
    +
    +/// detect a special case of "web browser"
    +Calendar.is_ie = ( /msie/i.test(navigator.userAgent) &&
    +		   !/opera/i.test(navigator.userAgent) );
    +
    +Calendar.is_ie5 = ( Calendar.is_ie && /msie 5\.0/i.test(navigator.userAgent) );
    +
    +/// detect Opera browser
    +Calendar.is_opera = /opera/i.test(navigator.userAgent);
    +
    +/// detect KHTML-based browsers
    +Calendar.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent);
    +
    +// BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate
    +//        library, at some point.
    +
    +Calendar.getAbsolutePos = function(el) {
    +	var SL = 0, ST = 0;
    +	var is_div = /^div$/i.test(el.tagName);
    +	if (is_div && el.scrollLeft)
    +		SL = el.scrollLeft;
    +	if (is_div && el.scrollTop)
    +		ST = el.scrollTop;
    +	var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
    +	if (el.offsetParent) {
    +		var tmp = this.getAbsolutePos(el.offsetParent);
    +		r.x += tmp.x;
    +		r.y += tmp.y;
    +	}
    +	return r;
    +};
    +
    +Calendar.isRelated = function (el, evt) {
    +	var related = evt.relatedTarget;
    +	if (!related) {
    +		var type = evt.type;
    +		if (type == "mouseover") {
    +			related = evt.fromElement;
    +		} else if (type == "mouseout") {
    +			related = evt.toElement;
    +		}
    +	}
    +	while (related) {
    +		if (related == el) {
    +			return true;
    +		}
    +		related = related.parentNode;
    +	}
    +	return false;
    +};
    +
    +Calendar.removeClass = function(el, className) {
    +	if (!(el && el.className)) {
    +		return;
    +	}
    +	var cls = el.className.split(" ");
    +	var ar = new Array();
    +	for (var i = cls.length; i > 0;) {
    +		if (cls[--i] != className) {
    +			ar[ar.length] = cls[i];
    +		}
    +	}
    +	el.className = ar.join(" ");
    +};
    +
    +Calendar.addClass = function(el, className) {
    +	Calendar.removeClass(el, className);
    +	el.className += " " + className;
    +};
    +
    +// FIXME: the following 2 functions totally suck, are useless and should be replaced immediately.
    +Calendar.getElement = function(ev) {
    +	var f = Calendar.is_ie ? window.event.srcElement : ev.currentTarget;
    +	while (f.nodeType != 1 || /^div$/i.test(f.tagName))
    +		f = f.parentNode;
    +	return f;
    +};
    +
    +Calendar.getTargetElement = function(ev) {
    +	var f = Calendar.is_ie ? window.event.srcElement : ev.target;
    +	while (f.nodeType != 1)
    +		f = f.parentNode;
    +	return f;
    +};
    +
    +Calendar.stopEvent = function(ev) {
    +	ev || (ev = window.event);
    +	if (Calendar.is_ie) {
    +		ev.cancelBubble = true;
    +		ev.returnValue = false;
    +	} else {
    +		ev.preventDefault();
    +		ev.stopPropagation();
    +	}
    +	return false;
    +};
    +
    +Calendar.addEvent = function(el, evname, func) {
    +	if (el.attachEvent) { // IE
    +		el.attachEvent("on" + evname, func);
    +	} else if (el.addEventListener) { // Gecko / W3C
    +		el.addEventListener(evname, func, true);
    +	} else {
    +		el["on" + evname] = func;
    +	}
    +};
    +
    +Calendar.removeEvent = function(el, evname, func) {
    +	if (el.detachEvent) { // IE
    +		el.detachEvent("on" + evname, func);
    +	} else if (el.removeEventListener) { // Gecko / W3C
    +		el.removeEventListener(evname, func, true);
    +	} else {
    +		el["on" + evname] = null;
    +	}
    +};
    +
    +Calendar.createElement = function(type, parent) {
    +	var el = null;
    +	if (document.createElementNS) {
    +		// use the XHTML namespace; IE won't normally get here unless
    +		// _they_ "fix" the DOM2 implementation.
    +		el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
    +	} else {
    +		el = document.createElement(type);
    +	}
    +	if (typeof parent != "undefined") {
    +		parent.appendChild(el);
    +	}
    +	return el;
    +};
    +
    +// END: UTILITY FUNCTIONS
    +
    +// BEGIN: CALENDAR STATIC FUNCTIONS
    +
    +/** Internal -- adds a set of events to make some element behave like a button. */
    +Calendar._add_evs = function(el) {
    +	with (Calendar) {
    +		addEvent(el, "mouseover", dayMouseOver);
    +		addEvent(el, "mousedown", dayMouseDown);
    +		addEvent(el, "mouseout", dayMouseOut);
    +		if (is_ie) {
    +			addEvent(el, "dblclick", dayMouseDblClick);
    +			el.setAttribute("unselectable", true);
    +		}
    +	}
    +};
    +
    +Calendar.findMonth = function(el) {
    +	if (typeof el.month != "undefined") {
    +		return el;
    +	} else if (typeof el.parentNode.month != "undefined") {
    +		return el.parentNode;
    +	}
    +	return null;
    +};
    +
    +Calendar.findYear = function(el) {
    +	if (typeof el.year != "undefined") {
    +		return el;
    +	} else if (typeof el.parentNode.year != "undefined") {
    +		return el.parentNode;
    +	}
    +	return null;
    +};
    +
    +Calendar.showMonthsCombo = function () {
    +	var cal = Calendar._C;
    +	if (!cal) {
    +		return false;
    +	}
    +	var cal = cal;
    +	var cd = cal.activeDiv;
    +	var mc = cal.monthsCombo;
    +	if (cal.hilitedMonth) {
    +		Calendar.removeClass(cal.hilitedMonth, "hilite");
    +	}
    +	if (cal.activeMonth) {
    +		Calendar.removeClass(cal.activeMonth, "active");
    +	}
    +	var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];
    +	Calendar.addClass(mon, "active");
    +	cal.activeMonth = mon;
    +	var s = mc.style;
    +	s.display = "block";
    +	if (cd.navtype < 0)
    +		s.left = cd.offsetLeft + "px";
    +	else {
    +		var mcw = mc.offsetWidth;
    +		if (typeof mcw == "undefined")
    +			// Konqueror brain-dead techniques
    +			mcw = 50;
    +		s.left = (cd.offsetLeft + cd.offsetWidth - mcw) + "px";
    +	}
    +	s.top = (cd.offsetTop + cd.offsetHeight) + "px";
    +};
    +
    +Calendar.showYearsCombo = function (fwd) {
    +	var cal = Calendar._C;
    +	if (!cal) {
    +		return false;
    +	}
    +	var cal = cal;
    +	var cd = cal.activeDiv;
    +	var yc = cal.yearsCombo;
    +	if (cal.hilitedYear) {
    +		Calendar.removeClass(cal.hilitedYear, "hilite");
    +	}
    +	if (cal.activeYear) {
    +		Calendar.removeClass(cal.activeYear, "active");
    +	}
    +	cal.activeYear = null;
    +	var Y = cal.date.getFullYear() + (fwd ? 1 : -1);
    +	var yr = yc.firstChild;
    +	var show = false;
    +	for (var i = 12; i > 0; --i) {
    +		if (Y >= cal.minYear && Y <= cal.maxYear) {
    +			yr.innerHTML = Y;
    +			yr.year = Y;
    +			yr.style.display = "block";
    +			show = true;
    +		} else {
    +			yr.style.display = "none";
    +		}
    +		yr = yr.nextSibling;
    +		Y += fwd ? cal.yearStep : -cal.yearStep;
    +	}
    +	if (show) {
    +		var s = yc.style;
    +		s.display = "block";
    +		if (cd.navtype < 0)
    +			s.left = cd.offsetLeft + "px";
    +		else {
    +			var ycw = yc.offsetWidth;
    +			if (typeof ycw == "undefined")
    +				// Konqueror brain-dead techniques
    +				ycw = 50;
    +			s.left = (cd.offsetLeft + cd.offsetWidth - ycw) + "px";
    +		}
    +		s.top = (cd.offsetTop + cd.offsetHeight) + "px";
    +	}
    +};
    +
    +// event handlers
    +
    +Calendar.tableMouseUp = function(ev) {
    +	var cal = Calendar._C;
    +	if (!cal) {
    +		return false;
    +	}
    +	if (cal.timeout) {
    +		clearTimeout(cal.timeout);
    +	}
    +	var el = cal.activeDiv;
    +	if (!el) {
    +		return false;
    +	}
    +	var target = Calendar.getTargetElement(ev);
    +	ev || (ev = window.event);
    +	Calendar.removeClass(el, "active");
    +	if (target == el || target.parentNode == el) {
    +		Calendar.cellClick(el, ev);
    +	}
    +	var mon = Calendar.findMonth(target);
    +	var date = null;
    +	if (mon) {
    +		date = new Date(cal.date);
    +		if (mon.month != date.getMonth()) {
    +			date.setMonth(mon.month);
    +			cal.setDate(date);
    +			cal.dateClicked = false;
    +			cal.callHandler();
    +		}
    +	} else {
    +		var year = Calendar.findYear(target);
    +		if (year) {
    +			date = new Date(cal.date);
    +			if (year.year != date.getFullYear()) {
    +				date.setFullYear(year.year);
    +				cal.setDate(date);
    +				cal.dateClicked = false;
    +				cal.callHandler();
    +			}
    +		}
    +	}
    +	with (Calendar) {
    +		removeEvent(document, "mouseup", tableMouseUp);
    +		removeEvent(document, "mouseover", tableMouseOver);
    +		removeEvent(document, "mousemove", tableMouseOver);
    +		cal._hideCombos();
    +		_C = null;
    +		return stopEvent(ev);
    +	}
    +};
    +
    +Calendar.tableMouseOver = function (ev) {
    +	var cal = Calendar._C;
    +	if (!cal) {
    +		return;
    +	}
    +	var el = cal.activeDiv;
    +	var target = Calendar.getTargetElement(ev);
    +	if (target == el || target.parentNode == el) {
    +		Calendar.addClass(el, "hilite active");
    +		Calendar.addClass(el.parentNode, "rowhilite");
    +	} else {
    +		if (typeof el.navtype == "undefined" || (el.navtype != 50 && (el.navtype == 0 || Math.abs(el.navtype) > 2)))
    +			Calendar.removeClass(el, "active");
    +		Calendar.removeClass(el, "hilite");
    +		Calendar.removeClass(el.parentNode, "rowhilite");
    +	}
    +	ev || (ev = window.event);
    +	if (el.navtype == 50 && target != el) {
    +		var pos = Calendar.getAbsolutePos(el);
    +		var w = el.offsetWidth;
    +		var x = ev.clientX;
    +		var dx;
    +		var decrease = true;
    +		if (x > pos.x + w) {
    +			dx = x - pos.x - w;
    +			decrease = false;
    +		} else
    +			dx = pos.x - x;
    +
    +		if (dx < 0) dx = 0;
    +		var range = el._range;
    +		var current = el._current;
    +		var count = Math.floor(dx / 10) % range.length;
    +		for (var i = range.length; --i >= 0;)
    +			if (range[i] == current)
    +				break;
    +		while (count-- > 0)
    +			if (decrease) {
    +				if (--i < 0)
    +					i = range.length - 1;
    +			} else if ( ++i >= range.length )
    +				i = 0;
    +		var newval = range[i];
    +		el.innerHTML = newval;
    +
    +		cal.onUpdateTime();
    +	}
    +	var mon = Calendar.findMonth(target);
    +	if (mon) {
    +		if (mon.month != cal.date.getMonth()) {
    +			if (cal.hilitedMonth) {
    +				Calendar.removeClass(cal.hilitedMonth, "hilite");
    +			}
    +			Calendar.addClass(mon, "hilite");
    +			cal.hilitedMonth = mon;
    +		} else if (cal.hilitedMonth) {
    +			Calendar.removeClass(cal.hilitedMonth, "hilite");
    +		}
    +	} else {
    +		if (cal.hilitedMonth) {
    +			Calendar.removeClass(cal.hilitedMonth, "hilite");
    +		}
    +		var year = Calendar.findYear(target);
    +		if (year) {
    +			if (year.year != cal.date.getFullYear()) {
    +				if (cal.hilitedYear) {
    +					Calendar.removeClass(cal.hilitedYear, "hilite");
    +				}
    +				Calendar.addClass(year, "hilite");
    +				cal.hilitedYear = year;
    +			} else if (cal.hilitedYear) {
    +				Calendar.removeClass(cal.hilitedYear, "hilite");
    +			}
    +		} else if (cal.hilitedYear) {
    +			Calendar.removeClass(cal.hilitedYear, "hilite");
    +		}
    +	}
    +	return Calendar.stopEvent(ev);
    +};
    +
    +Calendar.tableMouseDown = function (ev) {
    +	if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) {
    +		return Calendar.stopEvent(ev);
    +	}
    +};
    +
    +Calendar.calDragIt = function (ev) {
    +	var cal = Calendar._C;
    +	if (!(cal && cal.dragging)) {
    +		return false;
    +	}
    +	var posX;
    +	var posY;
    +	if (Calendar.is_ie) {
    +		posY = window.event.clientY + document.body.scrollTop;
    +		posX = window.event.clientX + document.body.scrollLeft;
    +	} else {
    +		posX = ev.pageX;
    +		posY = ev.pageY;
    +	}
    +	cal.hideShowCovered();
    +	var st = cal.element.style;
    +	st.left = (posX - cal.xOffs) + "px";
    +	st.top = (posY - cal.yOffs) + "px";
    +	return Calendar.stopEvent(ev);
    +};
    +
    +Calendar.calDragEnd = function (ev) {
    +	var cal = Calendar._C;
    +	if (!cal) {
    +		return false;
    +	}
    +	cal.dragging = false;
    +	with (Calendar) {
    +		removeEvent(document, "mousemove", calDragIt);
    +		removeEvent(document, "mouseup", calDragEnd);
    +		tableMouseUp(ev);
    +	}
    +	cal.hideShowCovered();
    +};
    +
    +Calendar.dayMouseDown = function(ev) {
    +	var el = Calendar.getElement(ev);
    +	if (el.disabled) {
    +		return false;
    +	}
    +	var cal = el.calendar;
    +	cal.activeDiv = el;
    +	Calendar._C = cal;
    +	if (el.navtype != 300) with (Calendar) {
    +		if (el.navtype == 50) {
    +			el._current = el.innerHTML;
    +			addEvent(document, "mousemove", tableMouseOver);
    +		} else
    +			addEvent(document, Calendar.is_ie5 ? "mousemove" : "mouseover", tableMouseOver);
    +		addClass(el, "hilite active");
    +		addEvent(document, "mouseup", tableMouseUp);
    +	} else if (cal.isPopup) {
    +		cal._dragStart(ev);
    +	}
    +	if (el.navtype == -1 || el.navtype == 1) {
    +		if (cal.timeout) clearTimeout(cal.timeout);
    +		cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250);
    +	} else if (el.navtype == -2 || el.navtype == 2) {
    +		if (cal.timeout) clearTimeout(cal.timeout);
    +		cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250);
    +	} else {
    +		cal.timeout = null;
    +	}
    +	return Calendar.stopEvent(ev);
    +};
    +
    +Calendar.dayMouseDblClick = function(ev) {
    +	Calendar.cellClick(Calendar.getElement(ev), ev || window.event);
    +	if (Calendar.is_ie) {
    +		document.selection.empty();
    +	}
    +};
    +
    +Calendar.dayMouseOver = function(ev) {
    +	var el = Calendar.getElement(ev);
    +	if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) {
    +		return false;
    +	}
    +	if (el.ttip) {
    +		if (el.ttip.substr(0, 1) == "_") {
    +			el.ttip = el.caldate.print(el.calendar.ttDateFormat) + el.ttip.substr(1);
    +		}
    +		el.calendar.tooltips.innerHTML = el.ttip;
    +	}
    +	if (el.navtype != 300) {
    +		Calendar.addClass(el, "hilite");
    +		if (el.caldate) {
    +			Calendar.addClass(el.parentNode, "rowhilite");
    +		}
    +	}
    +	return Calendar.stopEvent(ev);
    +};
    +
    +Calendar.dayMouseOut = function(ev) {
    +	with (Calendar) {
    +		var el = getElement(ev);
    +		if (isRelated(el, ev) || _C || el.disabled)
    +			return false;
    +		removeClass(el, "hilite");
    +		if (el.caldate)
    +			removeClass(el.parentNode, "rowhilite");
    +		if (el.calendar)
    +			el.calendar.tooltips.innerHTML = _TT["SEL_DATE"];
    +		return stopEvent(ev);
    +	}
    +};
    +
    +/**
    + *  A generic "click" handler :) handles all types of buttons defined in this
    + *  calendar.
    + */
    +Calendar.cellClick = function(el, ev) {
    +	var cal = el.calendar;
    +	var closing = false;
    +	var newdate = false;
    +	var date = null;
    +	if (typeof el.navtype == "undefined") {
    +		if (cal.currentDateEl) {
    +			Calendar.removeClass(cal.currentDateEl, "selected");
    +			Calendar.addClass(el, "selected");
    +			closing = (cal.currentDateEl == el);
    +			if (!closing) {
    +				cal.currentDateEl = el;
    +			}
    +		}
    +		cal.date.setDateOnly(el.caldate);
    +		date = cal.date;
    +		var other_month = !(cal.dateClicked = !el.otherMonth);
    +		if (!other_month && !cal.currentDateEl)
    +			cal._toggleMultipleDate(new Date(date));
    +		else
    +			newdate = !el.disabled;
    +		// a date was clicked
    +		if (other_month)
    +			cal._init(cal.firstDayOfWeek, date);
    +	} else {
    +		if (el.navtype == 200) {
    +			Calendar.removeClass(el, "hilite");
    +			cal.callCloseHandler();
    +			return;
    +		}
    +		date = new Date(cal.date);
    +		if (el.navtype == 0)
    +			date.setDateOnly(new Date()); // TODAY
    +		// unless "today" was clicked, we assume no date was clicked so
    +		// the selected handler will know not to close the calenar when
    +		// in single-click mode.
    +		// cal.dateClicked = (el.navtype == 0);
    +		cal.dateClicked = false;
    +		var year = date.getFullYear();
    +		var mon = date.getMonth();
    +		function setMonth(m) {
    +			var day = date.getDate();
    +			var max = date.getMonthDays(m);
    +			if (day > max) {
    +				date.setDate(max);
    +			}
    +			date.setMonth(m);
    +		};
    +		switch (el.navtype) {
    +		    case 400:
    +			Calendar.removeClass(el, "hilite");
    +			var text = Calendar._TT["ABOUT"];
    +			if (typeof text != "undefined") {
    +				text += cal.showsTime ? Calendar._TT["ABOUT_TIME"] : "";
    +			} else {
    +				// FIXME: this should be removed as soon as lang files get updated!
    +				text = "Help and about box text is not translated into this language.\n" +
    +					"If you know this language and you feel generous please update\n" +
    +					"the corresponding file in \"lang\" subdir to match calendar-en.js\n" +
    +					"and send it back to  to get it into the distribution  ;-)\n\n" +
    +					"Thank you!\n" +
    +					"http://dynarch.com/mishoo/calendar.epl\n";
    +			}
    +			alert(text);
    +			return;
    +		    case -2:
    +			if (year > cal.minYear) {
    +				date.setFullYear(year - 1);
    +			}
    +			break;
    +		    case -1:
    +			if (mon > 0) {
    +				setMonth(mon - 1);
    +			} else if (year-- > cal.minYear) {
    +				date.setFullYear(year);
    +				setMonth(11);
    +			}
    +			break;
    +		    case 1:
    +			if (mon < 11) {
    +				setMonth(mon + 1);
    +			} else if (year < cal.maxYear) {
    +				date.setFullYear(year + 1);
    +				setMonth(0);
    +			}
    +			break;
    +		    case 2:
    +			if (year < cal.maxYear) {
    +				date.setFullYear(year + 1);
    +			}
    +			break;
    +		    case 100:
    +			cal.setFirstDayOfWeek(el.fdow);
    +			return;
    +		    case 50:
    +			var range = el._range;
    +			var current = el.innerHTML;
    +			for (var i = range.length; --i >= 0;)
    +				if (range[i] == current)
    +					break;
    +			if (ev && ev.shiftKey) {
    +				if (--i < 0)
    +					i = range.length - 1;
    +			} else if ( ++i >= range.length )
    +				i = 0;
    +			var newval = range[i];
    +			el.innerHTML = newval;
    +			cal.onUpdateTime();
    +			return;
    +		    case 0:
    +			// TODAY will bring us here
    +			if ((typeof cal.getDateStatus == "function") &&
    +			    cal.getDateStatus(date, date.getFullYear(), date.getMonth(), date.getDate())) {
    +				return false;
    +			}
    +			break;
    +		}
    +		if (!date.equalsTo(cal.date)) {
    +			cal.setDate(date);
    +			newdate = true;
    +		} else if (el.navtype == 0)
    +			newdate = closing = true;
    +	}
    +	if (newdate) {
    +		ev && cal.callHandler();
    +	}
    +	if (closing) {
    +		Calendar.removeClass(el, "hilite");
    +		ev && cal.callCloseHandler();
    +	}
    +};
    +
    +// END: CALENDAR STATIC FUNCTIONS
    +
    +// BEGIN: CALENDAR OBJECT FUNCTIONS
    +
    +/**
    + *  This function creates the calendar inside the given parent.  If _par is
    + *  null than it creates a popup calendar inside the BODY element.  If _par is
    + *  an element, be it BODY, then it creates a non-popup calendar (still
    + *  hidden).  Some properties need to be set before calling this function.
    + */
    +Calendar.prototype.create = function (_par) {
    +	var parent = null;
    +	if (! _par) {
    +		// default parent is the document body, in which case we create
    +		// a popup calendar.
    +		parent = document.getElementsByTagName("body")[0];
    +		this.isPopup = true;
    +	} else {
    +		parent = _par;
    +		this.isPopup = false;
    +	}
    +	this.date = this.dateStr ? new Date(this.dateStr) : new Date();
    +
    +	var table = Calendar.createElement("table");
    +	this.table = table;
    +	table.cellSpacing = 0;
    +	table.cellPadding = 0;
    +	table.calendar = this;
    +	Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown);
    +
    +	var div = Calendar.createElement("div");
    +	this.element = div;
    +	div.className = "calendar";
    +	if (this.isPopup) {
    +		div.style.position = "absolute";
    +		div.style.display = "none";
    +	}
    +	div.appendChild(table);
    +
    +	var thead = Calendar.createElement("thead", table);
    +	var cell = null;
    +	var row = null;
    +
    +	var cal = this;
    +	var hh = function (text, cs, navtype) {
    +		cell = Calendar.createElement("td", row);
    +		cell.colSpan = cs;
    +		cell.className = "button";
    +		if (navtype != 0 && Math.abs(navtype) <= 2)
    +			cell.className += " nav";
    +		Calendar._add_evs(cell);
    +		cell.calendar = cal;
    +		cell.navtype = navtype;
    +		cell.innerHTML = "
    " + text + "
    "; + return cell; + }; + + row = Calendar.createElement("tr", thead); + var title_length = 6; + (this.isPopup) && --title_length; + (this.weekNumbers) && ++title_length; + + hh("?", 1, 400).ttip = Calendar._TT["INFO"]; + this.title = hh("", title_length, 300); + this.title.className = "title"; + if (this.isPopup) { + this.title.ttip = Calendar._TT["DRAG_TO_MOVE"]; + this.title.style.cursor = "move"; + hh("×", 1, 200).ttip = Calendar._TT["CLOSE"]; + } + + row = Calendar.createElement("tr", thead); + row.className = "headrow"; + + this._nav_py = hh("«", 1, -2); + this._nav_py.ttip = Calendar._TT["PREV_YEAR"]; + + this._nav_pm = hh("‹", 1, -1); + this._nav_pm.ttip = Calendar._TT["PREV_MONTH"]; + + this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0); + this._nav_now.ttip = Calendar._TT["GO_TODAY"]; + + this._nav_nm = hh("›", 1, 1); + this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"]; + + this._nav_ny = hh("»", 1, 2); + this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"]; + + // day names + row = Calendar.createElement("tr", thead); + row.className = "daynames"; + if (this.weekNumbers) { + cell = Calendar.createElement("td", row); + cell.className = "name wn"; + cell.innerHTML = Calendar._TT["WK"]; + } + for (var i = 7; i > 0; --i) { + cell = Calendar.createElement("td", row); + if (!i) { + cell.navtype = 100; + cell.calendar = this; + Calendar._add_evs(cell); + } + } + this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild; + this._displayWeekdays(); + + var tbody = Calendar.createElement("tbody", table); + this.tbody = tbody; + + for (i = 6; i > 0; --i) { + row = Calendar.createElement("tr", tbody); + if (this.weekNumbers) { + cell = Calendar.createElement("td", row); + } + for (var j = 7; j > 0; --j) { + cell = Calendar.createElement("td", row); + cell.calendar = this; + Calendar._add_evs(cell); + } + } + + if (this.showsTime) { + row = Calendar.createElement("tr", tbody); + row.className = "time"; + + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = 2; + cell.innerHTML = Calendar._TT["TIME"] || " "; + + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = this.weekNumbers ? 4 : 3; + + (function(){ + function makeTimePart(className, init, range_start, range_end) { + var part = Calendar.createElement("span", cell); + part.className = className; + part.innerHTML = init; + part.calendar = cal; + part.ttip = Calendar._TT["TIME_PART"]; + part.navtype = 50; + part._range = []; + if (typeof range_start != "number") + part._range = range_start; + else { + for (var i = range_start; i <= range_end; ++i) { + var txt; + if (i < 10 && range_end >= 10) txt = '0' + i; + else txt = '' + i; + part._range[part._range.length] = txt; + } + } + Calendar._add_evs(part); + return part; + }; + var hrs = cal.date.getHours(); + var mins = cal.date.getMinutes(); + var t12 = !cal.time24; + var pm = (hrs > 12); + if (t12 && pm) hrs -= 12; + var H = makeTimePart("hour", hrs, t12 ? 1 : 0, t12 ? 12 : 23); + var span = Calendar.createElement("span", cell); + span.innerHTML = ":"; + span.className = "colon"; + var M = makeTimePart("minute", mins, 0, 59); + var AP = null; + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = 2; + if (t12) + AP = makeTimePart("ampm", pm ? "pm" : "am", ["am", "pm"]); + else + cell.innerHTML = " "; + + cal.onSetTime = function() { + var pm, hrs = this.date.getHours(), + mins = this.date.getMinutes(); + if (t12) { + pm = (hrs >= 12); + if (pm) hrs -= 12; + if (hrs == 0) hrs = 12; + AP.innerHTML = pm ? "pm" : "am"; + } + H.innerHTML = (hrs < 10) ? ("0" + hrs) : hrs; + M.innerHTML = (mins < 10) ? ("0" + mins) : mins; + }; + + cal.onUpdateTime = function() { + var date = this.date; + var h = parseInt(H.innerHTML, 10); + if (t12) { + if (/pm/i.test(AP.innerHTML) && h < 12) + h += 12; + else if (/am/i.test(AP.innerHTML) && h == 12) + h = 0; + } + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + date.setHours(h); + date.setMinutes(parseInt(M.innerHTML, 10)); + date.setFullYear(y); + date.setMonth(m); + date.setDate(d); + this.dateClicked = false; + this.callHandler(); + }; + })(); + } else { + this.onSetTime = this.onUpdateTime = function() {}; + } + + var tfoot = Calendar.createElement("tfoot", table); + + row = Calendar.createElement("tr", tfoot); + row.className = "footrow"; + + cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300); + cell.className = "ttip"; + if (this.isPopup) { + cell.ttip = Calendar._TT["DRAG_TO_MOVE"]; + cell.style.cursor = "move"; + } + this.tooltips = cell; + + div = Calendar.createElement("div", this.element); + this.monthsCombo = div; + div.className = "combo"; + for (i = 0; i < Calendar._MN.length; ++i) { + var mn = Calendar.createElement("div"); + mn.className = Calendar.is_ie ? "label-IEfix" : "label"; + mn.month = i; + mn.innerHTML = Calendar._SMN[i]; + div.appendChild(mn); + } + + div = Calendar.createElement("div", this.element); + this.yearsCombo = div; + div.className = "combo"; + for (i = 12; i > 0; --i) { + var yr = Calendar.createElement("div"); + yr.className = Calendar.is_ie ? "label-IEfix" : "label"; + div.appendChild(yr); + } + + this._init(this.firstDayOfWeek, this.date); + parent.appendChild(this.element); +}; + +/** keyboard navigation, only for popup calendars */ +Calendar._keyEvent = function(ev) { + var cal = window._dynarch_popupCalendar; + if (!cal || cal.multiple) + return false; + (Calendar.is_ie) && (ev = window.event); + var act = (Calendar.is_ie || ev.type == "keypress"), + K = ev.keyCode; + if (ev.ctrlKey) { + switch (K) { + case 37: // KEY left + act && Calendar.cellClick(cal._nav_pm); + break; + case 38: // KEY up + act && Calendar.cellClick(cal._nav_py); + break; + case 39: // KEY right + act && Calendar.cellClick(cal._nav_nm); + break; + case 40: // KEY down + act && Calendar.cellClick(cal._nav_ny); + break; + default: + return false; + } + } else switch (K) { + case 32: // KEY space (now) + Calendar.cellClick(cal._nav_now); + break; + case 27: // KEY esc + act && cal.callCloseHandler(); + break; + case 37: // KEY left + case 38: // KEY up + case 39: // KEY right + case 40: // KEY down + if (act) { + var prev, x, y, ne, el, step; + prev = K == 37 || K == 38; + step = (K == 37 || K == 39) ? 1 : 7; + function setVars() { + el = cal.currentDateEl; + var p = el.pos; + x = p & 15; + y = p >> 4; + ne = cal.ar_days[y][x]; + };setVars(); + function prevMonth() { + var date = new Date(cal.date); + date.setDate(date.getDate() - step); + cal.setDate(date); + }; + function nextMonth() { + var date = new Date(cal.date); + date.setDate(date.getDate() + step); + cal.setDate(date); + }; + while (1) { + switch (K) { + case 37: // KEY left + if (--x >= 0) + ne = cal.ar_days[y][x]; + else { + x = 6; + K = 38; + continue; + } + break; + case 38: // KEY up + if (--y >= 0) + ne = cal.ar_days[y][x]; + else { + prevMonth(); + setVars(); + } + break; + case 39: // KEY right + if (++x < 7) + ne = cal.ar_days[y][x]; + else { + x = 0; + K = 40; + continue; + } + break; + case 40: // KEY down + if (++y < cal.ar_days.length) + ne = cal.ar_days[y][x]; + else { + nextMonth(); + setVars(); + } + break; + } + break; + } + if (ne) { + if (!ne.disabled) + Calendar.cellClick(ne); + else if (prev) + prevMonth(); + else + nextMonth(); + } + } + break; + case 13: // KEY enter + if (act) + Calendar.cellClick(cal.currentDateEl, ev); + break; + default: + return false; + } + return Calendar.stopEvent(ev); +}; + +/** + * (RE)Initializes the calendar to the given date and firstDayOfWeek + */ +Calendar.prototype._init = function (firstDayOfWeek, date) { + var today = new Date(), + TY = today.getFullYear(), + TM = today.getMonth(), + TD = today.getDate(); + this.table.style.visibility = "hidden"; + var year = date.getFullYear(); + if (year < this.minYear) { + year = this.minYear; + date.setFullYear(year); + } else if (year > this.maxYear) { + year = this.maxYear; + date.setFullYear(year); + } + this.firstDayOfWeek = firstDayOfWeek; + this.date = new Date(date); + var month = date.getMonth(); + var mday = date.getDate(); + var no_days = date.getMonthDays(); + + // calendar voodoo for computing the first day that would actually be + // displayed in the calendar, even if it's from the previous month. + // WARNING: this is magic. ;-) + date.setDate(1); + var day1 = (date.getDay() - this.firstDayOfWeek) % 7; + if (day1 < 0) + day1 += 7; + date.setDate(-day1); + date.setDate(date.getDate() + 1); + + var row = this.tbody.firstChild; + var MN = Calendar._SMN[month]; + var ar_days = this.ar_days = new Array(); + var weekend = Calendar._TT["WEEKEND"]; + var dates = this.multiple ? (this.datesCells = {}) : null; + for (var i = 0; i < 6; ++i, row = row.nextSibling) { + var cell = row.firstChild; + if (this.weekNumbers) { + cell.className = "day wn"; + cell.innerHTML = date.getWeekNumber(); + cell = cell.nextSibling; + } + row.className = "daysrow"; + var hasdays = false, iday, dpos = ar_days[i] = []; + for (var j = 0; j < 7; ++j, cell = cell.nextSibling, date.setDate(iday + 1)) { + iday = date.getDate(); + var wday = date.getDay(); + cell.className = "day"; + cell.pos = i << 4 | j; + dpos[j] = cell; + var current_month = (date.getMonth() == month); + if (!current_month) { + if (this.showsOtherMonths) { + cell.className += " othermonth"; + cell.otherMonth = true; + } else { + cell.className = "emptycell"; + cell.innerHTML = " "; + cell.disabled = true; + continue; + } + } else { + cell.otherMonth = false; + hasdays = true; + } + cell.disabled = false; + cell.innerHTML = this.getDateText ? this.getDateText(date, iday) : iday; + if (dates) + dates[date.print("%Y%m%d")] = cell; + if (this.getDateStatus) { + var status = this.getDateStatus(date, year, month, iday); + if (this.getDateToolTip) { + var toolTip = this.getDateToolTip(date, year, month, iday); + if (toolTip) + cell.title = toolTip; + } + if (status === true) { + cell.className += " disabled"; + cell.disabled = true; + } else { + if (/disabled/i.test(status)) + cell.disabled = true; + cell.className += " " + status; + } + } + if (!cell.disabled) { + cell.caldate = new Date(date); + cell.ttip = "_"; + if (!this.multiple && current_month + && iday == mday && this.hiliteToday) { + cell.className += " selected"; + this.currentDateEl = cell; + } + if (date.getFullYear() == TY && + date.getMonth() == TM && + iday == TD) { + cell.className += " today"; + cell.ttip += Calendar._TT["PART_TODAY"]; + } + if (weekend.indexOf(wday.toString()) != -1) + cell.className += cell.otherMonth ? " oweekend" : " weekend"; + } + } + if (!(hasdays || this.showsOtherMonths)) + row.className = "emptyrow"; + } + this.title.innerHTML = Calendar._MN[month] + ", " + year; + this.onSetTime(); + this.table.style.visibility = "visible"; + this._initMultipleDates(); + // PROFILE + // this.tooltips.innerHTML = "Generated in " + ((new Date()) - today) + " ms"; +}; + +Calendar.prototype._initMultipleDates = function() { + if (this.multiple) { + for (var i in this.multiple) { + var cell = this.datesCells[i]; + var d = this.multiple[i]; + if (!d) + continue; + if (cell) + cell.className += " selected"; + } + } +}; + +Calendar.prototype._toggleMultipleDate = function(date) { + if (this.multiple) { + var ds = date.print("%Y%m%d"); + var cell = this.datesCells[ds]; + if (cell) { + var d = this.multiple[ds]; + if (!d) { + Calendar.addClass(cell, "selected"); + this.multiple[ds] = date; + } else { + Calendar.removeClass(cell, "selected"); + delete this.multiple[ds]; + } + } + } +}; + +Calendar.prototype.setDateToolTipHandler = function (unaryFunction) { + this.getDateToolTip = unaryFunction; +}; + +/** + * Calls _init function above for going to a certain date (but only if the + * date is different than the currently selected one). + */ +Calendar.prototype.setDate = function (date) { + if (!date.equalsTo(this.date)) { + this._init(this.firstDayOfWeek, date); + } +}; + +/** + * Refreshes the calendar. Useful if the "disabledHandler" function is + * dynamic, meaning that the list of disabled date can change at runtime. + * Just * call this function if you think that the list of disabled dates + * should * change. + */ +Calendar.prototype.refresh = function () { + this._init(this.firstDayOfWeek, this.date); +}; + +/** Modifies the "firstDayOfWeek" parameter (pass 0 for Synday, 1 for Monday, etc.). */ +Calendar.prototype.setFirstDayOfWeek = function (firstDayOfWeek) { + this._init(firstDayOfWeek, this.date); + this._displayWeekdays(); +}; + +/** + * Allows customization of what dates are enabled. The "unaryFunction" + * parameter must be a function object that receives the date (as a JS Date + * object) and returns a boolean value. If the returned value is true then + * the passed date will be marked as disabled. + */ +Calendar.prototype.setDateStatusHandler = Calendar.prototype.setDisabledHandler = function (unaryFunction) { + this.getDateStatus = unaryFunction; +}; + +/** Customization of allowed year range for the calendar. */ +Calendar.prototype.setRange = function (a, z) { + this.minYear = a; + this.maxYear = z; +}; + +/** Calls the first user handler (selectedHandler). */ +Calendar.prototype.callHandler = function () { + if (this.onSelected) { + this.onSelected(this, this.date.print(this.dateFormat)); + } +}; + +/** Calls the second user handler (closeHandler). */ +Calendar.prototype.callCloseHandler = function () { + if (this.onClose) { + this.onClose(this); + } + this.hideShowCovered(); +}; + +/** Removes the calendar object from the DOM tree and destroys it. */ +Calendar.prototype.destroy = function () { + var el = this.element.parentNode; + el.removeChild(this.element); + Calendar._C = null; + window._dynarch_popupCalendar = null; +}; + +/** + * Moves the calendar element to a different section in the DOM tree (changes + * its parent). + */ +Calendar.prototype.reparent = function (new_parent) { + var el = this.element; + el.parentNode.removeChild(el); + new_parent.appendChild(el); +}; + +// This gets called when the user presses a mouse button anywhere in the +// document, if the calendar is shown. If the click was outside the open +// calendar this function closes it. +Calendar._checkCalendar = function(ev) { + var calendar = window._dynarch_popupCalendar; + if (!calendar) { + return false; + } + var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev); + for (; el != null && el != calendar.element; el = el.parentNode); + if (el == null) { + // calls closeHandler which should hide the calendar. + window._dynarch_popupCalendar.callCloseHandler(); + return Calendar.stopEvent(ev); + } +}; + +/** Shows the calendar. */ +Calendar.prototype.show = function () { + var rows = this.table.getElementsByTagName("tr"); + for (var i = rows.length; i > 0;) { + var row = rows[--i]; + Calendar.removeClass(row, "rowhilite"); + var cells = row.getElementsByTagName("td"); + for (var j = cells.length; j > 0;) { + var cell = cells[--j]; + Calendar.removeClass(cell, "hilite"); + Calendar.removeClass(cell, "active"); + } + } + this.element.style.display = "block"; + this.hidden = false; + if (this.isPopup) { + window._dynarch_popupCalendar = this; + Calendar.addEvent(document, "keydown", Calendar._keyEvent); + Calendar.addEvent(document, "keypress", Calendar._keyEvent); + Calendar.addEvent(document, "mousedown", Calendar._checkCalendar); + } + this.hideShowCovered(); +}; + +/** + * Hides the calendar. Also removes any "hilite" from the class of any TD + * element. + */ +Calendar.prototype.hide = function () { + if (this.isPopup) { + Calendar.removeEvent(document, "keydown", Calendar._keyEvent); + Calendar.removeEvent(document, "keypress", Calendar._keyEvent); + Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar); + } + this.element.style.display = "none"; + this.hidden = true; + this.hideShowCovered(); +}; + +/** + * Shows the calendar at a given absolute position (beware that, depending on + * the calendar element style -- position property -- this might be relative + * to the parent's containing rectangle). + */ +Calendar.prototype.showAt = function (x, y) { + var s = this.element.style; + s.left = x + "px"; + s.top = y + "px"; + this.show(); +}; + +/** Shows the calendar near a given element. */ +Calendar.prototype.showAtElement = function (el, opts) { + var self = this; + var p = Calendar.getAbsolutePos(el); + if (!opts || typeof opts != "string") { + this.showAt(p.x, p.y + el.offsetHeight); + return true; + } + function fixPosition(box) { + if (box.x < 0) + box.x = 0; + if (box.y < 0) + box.y = 0; + var cp = document.createElement("div"); + var s = cp.style; + s.position = "absolute"; + s.right = s.bottom = s.width = s.height = "0px"; + document.body.appendChild(cp); + var br = Calendar.getAbsolutePos(cp); + document.body.removeChild(cp); + if (Calendar.is_ie) { + br.y += document.body.scrollTop; + br.x += document.body.scrollLeft; + } else { + br.y += window.scrollY; + br.x += window.scrollX; + } + var tmp = box.x + box.width - br.x; + if (tmp > 0) box.x -= tmp; + tmp = box.y + box.height - br.y; + if (tmp > 0) box.y -= tmp; + }; + this.element.style.display = "block"; + Calendar.continuation_for_the_fucking_khtml_browser = function() { + var w = self.element.offsetWidth; + var h = self.element.offsetHeight; + self.element.style.display = "none"; + var valign = opts.substr(0, 1); + var halign = "l"; + if (opts.length > 1) { + halign = opts.substr(1, 1); + } + // vertical alignment + switch (valign) { + case "T": p.y -= h; break; + case "B": p.y += el.offsetHeight; break; + case "C": p.y += (el.offsetHeight - h) / 2; break; + case "t": p.y += el.offsetHeight - h; break; + case "b": break; // already there + } + // horizontal alignment + switch (halign) { + case "L": p.x -= w; break; + case "R": p.x += el.offsetWidth; break; + case "C": p.x += (el.offsetWidth - w) / 2; break; + case "l": p.x += el.offsetWidth - w; break; + case "r": break; // already there + } + p.width = w; + p.height = h + 40; + self.monthsCombo.style.display = "none"; + fixPosition(p); + self.showAt(p.x, p.y); + }; + if (Calendar.is_khtml) + setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10); + else + Calendar.continuation_for_the_fucking_khtml_browser(); +}; + +/** Customizes the date format. */ +Calendar.prototype.setDateFormat = function (str) { + this.dateFormat = str; +}; + +/** Customizes the tooltip date format. */ +Calendar.prototype.setTtDateFormat = function (str) { + this.ttDateFormat = str; +}; + +/** + * Tries to identify the date represented in a string. If successful it also + * calls this.setDate which moves the calendar to the given date. + */ +Calendar.prototype.parseDate = function(str, fmt) { + if (!fmt) + fmt = this.dateFormat; + this.setDate(Date.parseDate(str, fmt)); +}; + +Calendar.prototype.hideShowCovered = function () { + if (!Calendar.is_ie && !Calendar.is_opera) + return; + function getVisib(obj){ + var value = obj.style.visibility; + if (!value) { + if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C + if (!Calendar.is_khtml) + value = document.defaultView. + getComputedStyle(obj, "").getPropertyValue("visibility"); + else + value = ''; + } else if (obj.currentStyle) { // IE + value = obj.currentStyle.visibility; + } else + value = ''; + } + return value; + }; + + var tags = new Array("applet", "iframe", "select"); + var el = this.element; + + var p = Calendar.getAbsolutePos(el); + var EX1 = p.x; + var EX2 = el.offsetWidth + EX1; + var EY1 = p.y; + var EY2 = el.offsetHeight + EY1; + + for (var k = tags.length; k > 0; ) { + var ar = document.getElementsByTagName(tags[--k]); + var cc = null; + + for (var i = ar.length; i > 0;) { + cc = ar[--i]; + + p = Calendar.getAbsolutePos(cc); + var CX1 = p.x; + var CX2 = cc.offsetWidth + CX1; + var CY1 = p.y; + var CY2 = cc.offsetHeight + CY1; + + if (this.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) { + if (!cc.__msh_save_visibility) { + cc.__msh_save_visibility = getVisib(cc); + } + cc.style.visibility = cc.__msh_save_visibility; + } else { + if (!cc.__msh_save_visibility) { + cc.__msh_save_visibility = getVisib(cc); + } + cc.style.visibility = "hidden"; + } + } + } +}; + +/** Internal function; it displays the bar with the names of the weekday. */ +Calendar.prototype._displayWeekdays = function () { + var fdow = this.firstDayOfWeek; + var cell = this.firstdayname; + var weekend = Calendar._TT["WEEKEND"]; + for (var i = 0; i < 7; ++i) { + cell.className = "day name"; + var realday = (i + fdow) % 7; + if (i) { + cell.ttip = Calendar._TT["DAY_FIRST"].replace("%s", Calendar._DN[realday]); + cell.navtype = 100; + cell.calendar = this; + cell.fdow = realday; + Calendar._add_evs(cell); + } + if (weekend.indexOf(realday.toString()) != -1) { + Calendar.addClass(cell, "weekend"); + } + cell.innerHTML = Calendar._SDN[(i + fdow) % 7]; + cell = cell.nextSibling; + } +}; + +/** Internal function. Hides all combo boxes that might be displayed. */ +Calendar.prototype._hideCombos = function () { + this.monthsCombo.style.display = "none"; + this.yearsCombo.style.display = "none"; +}; + +/** Internal function. Starts dragging the element. */ +Calendar.prototype._dragStart = function (ev) { + if (this.dragging) { + return; + } + this.dragging = true; + var posX; + var posY; + if (Calendar.is_ie) { + posY = window.event.clientY + document.body.scrollTop; + posX = window.event.clientX + document.body.scrollLeft; + } else { + posY = ev.clientY + window.scrollY; + posX = ev.clientX + window.scrollX; + } + var st = this.element.style; + this.xOffs = posX - parseInt(st.left); + this.yOffs = posY - parseInt(st.top); + with (Calendar) { + addEvent(document, "mousemove", calDragIt); + addEvent(document, "mouseup", calDragEnd); + } +}; + +// BEGIN: DATE OBJECT PATCHES + +/** Adds the number of days array to the Date object. */ +Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31); + +/** Constants used for time computations */ +Date.SECOND = 1000 /* milliseconds */; +Date.MINUTE = 60 * Date.SECOND; +Date.HOUR = 60 * Date.MINUTE; +Date.DAY = 24 * Date.HOUR; +Date.WEEK = 7 * Date.DAY; + +Date.parseDate = function(str, fmt) { + var today = new Date(); + var y = 0; + var m = -1; + var d = 0; + var a = str.split(/\W+/); + var b = fmt.match(/%./g); + var i = 0, j = 0; + var hr = 0; + var min = 0; + for (i = 0; i < a.length; ++i) { + if (!a[i]) + continue; + switch (b[i]) { + case "%d": + case "%e": + d = parseInt(a[i], 10); + break; + + case "%m": + m = parseInt(a[i], 10) - 1; + break; + + case "%Y": + case "%y": + y = parseInt(a[i], 10); + (y < 100) && (y += (y > 29) ? 1900 : 2000); + break; + + case "%b": + case "%B": + for (j = 0; j < 12; ++j) { + if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; } + } + break; + + case "%H": + case "%I": + case "%k": + case "%l": + hr = parseInt(a[i], 10); + break; + + case "%P": + case "%p": + if (/pm/i.test(a[i]) && hr < 12) + hr += 12; + else if (/am/i.test(a[i]) && hr >= 12) + hr -= 12; + break; + + case "%M": + min = parseInt(a[i], 10); + break; + } + } + if (isNaN(y)) y = today.getFullYear(); + if (isNaN(m)) m = today.getMonth(); + if (isNaN(d)) d = today.getDate(); + if (isNaN(hr)) hr = today.getHours(); + if (isNaN(min)) min = today.getMinutes(); + if (y != 0 && m != -1 && d != 0) + return new Date(y, m, d, hr, min, 0); + y = 0; m = -1; d = 0; + for (i = 0; i < a.length; ++i) { + if (a[i].search(/[a-zA-Z]+/) != -1) { + var t = -1; + for (j = 0; j < 12; ++j) { + if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; } + } + if (t != -1) { + if (m != -1) { + d = m+1; + } + m = t; + } + } else if (parseInt(a[i], 10) <= 12 && m == -1) { + m = a[i]-1; + } else if (parseInt(a[i], 10) > 31 && y == 0) { + y = parseInt(a[i], 10); + (y < 100) && (y += (y > 29) ? 1900 : 2000); + } else if (d == 0) { + d = a[i]; + } + } + if (y == 0) + y = today.getFullYear(); + if (m != -1 && d != 0) + return new Date(y, m, d, hr, min, 0); + return today; +}; + +/** Returns the number of days in the current month */ +Date.prototype.getMonthDays = function(month) { + var year = this.getFullYear(); + if (typeof month == "undefined") { + month = this.getMonth(); + } + if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) { + return 29; + } else { + return Date._MD[month]; + } +}; + +/** Returns the number of day in the year. */ +Date.prototype.getDayOfYear = function() { + var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0); + var then = new Date(this.getFullYear(), 0, 0, 0, 0, 0); + var time = now - then; + return Math.floor(time / Date.DAY); +}; + +/** Returns the number of the week in year, as defined in ISO 8601. */ +Date.prototype.getWeekNumber = function() { + var d = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0); + var DoW = d.getDay(); + d.setDate(d.getDate() - (DoW + 6) % 7 + 3); // Nearest Thu + var ms = d.valueOf(); // GMT + d.setMonth(0); + d.setDate(4); // Thu in Week 1 + return Math.round((ms - d.valueOf()) / (7 * 864e5)) + 1; +}; + +/** Checks date and time equality */ +Date.prototype.equalsTo = function(date) { + return ((this.getFullYear() == date.getFullYear()) && + (this.getMonth() == date.getMonth()) && + (this.getDate() == date.getDate()) && + (this.getHours() == date.getHours()) && + (this.getMinutes() == date.getMinutes())); +}; + +/** Set only the year, month, date parts (keep existing time) */ +Date.prototype.setDateOnly = function(date) { + var tmp = new Date(date); + this.setDate(1); + this.setFullYear(tmp.getFullYear()); + this.setMonth(tmp.getMonth()); + this.setDate(tmp.getDate()); +}; + +/** Prints the date in a string according to the given format. */ +Date.prototype.print = function (str) { + var m = this.getMonth(); + var d = this.getDate(); + var y = this.getFullYear(); + var wn = this.getWeekNumber(); + var w = this.getDay(); + var s = {}; + var hr = this.getHours(); + var pm = (hr >= 12); + var ir = (pm) ? (hr - 12) : hr; + var dy = this.getDayOfYear(); + if (ir == 0) + ir = 12; + var min = this.getMinutes(); + var sec = this.getSeconds(); + s["%a"] = Calendar._SDN[w]; // abbreviated weekday name [FIXME: I18N] + s["%A"] = Calendar._DN[w]; // full weekday name + s["%b"] = Calendar._SMN[m]; // abbreviated month name [FIXME: I18N] + s["%B"] = Calendar._MN[m]; // full month name + // FIXME: %c : preferred date and time representation for the current locale + s["%C"] = 1 + Math.floor(y / 100); // the century number + s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31) + s["%e"] = d; // the day of the month (range 1 to 31) + // FIXME: %D : american date style: %m/%d/%y + // FIXME: %E, %F, %G, %g, %h (man strftime) + s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format) + s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format) + s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366) + s["%k"] = hr; // hour, range 0 to 23 (24h format) + s["%l"] = ir; // hour, range 1 to 12 (12h format) + s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12 + s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59 + s["%n"] = "\n"; // a newline character + s["%p"] = pm ? "PM" : "AM"; + s["%P"] = pm ? "pm" : "am"; + // FIXME: %r : the time in am/pm notation %I:%M:%S %p + // FIXME: %R : the time in 24-hour notation %H:%M + s["%s"] = Math.floor(this.getTime() / 1000); + s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59 + s["%t"] = "\t"; // a tab character + // FIXME: %T : the time in 24-hour notation (%H:%M:%S) + s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn; + s["%u"] = w + 1; // the day of the week (range 1 to 7, 1 = MON) + s["%w"] = w; // the day of the week (range 0 to 6, 0 = SUN) + // FIXME: %x : preferred date representation for the current locale without the time + // FIXME: %X : preferred time representation for the current locale without the date + s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99) + s["%Y"] = y; // year with the century + s["%%"] = "%"; // a literal '%' character + + var re = /%./g; + if (!Calendar.is_ie5 && !Calendar.is_khtml) + return str.replace(re, function (par) { return s[par] || par; }); + + var a = str.match(re); + for (var i = 0; i < a.length; i++) { + var tmp = s[a[i]]; + if (tmp) { + re = new RegExp(a[i], 'g'); + str = str.replace(re, tmp); + } + } + + return str; +}; + +Date.prototype.__msh_oldSetFullYear = Date.prototype.setFullYear; +Date.prototype.setFullYear = function(y) { + var d = new Date(this); + d.__msh_oldSetFullYear(y); + if (d.getMonth() != this.getMonth()) + this.setDate(28); + this.__msh_oldSetFullYear(y); +}; + +// END: DATE OBJECT PATCHES + + +// global object that remembers the calendar +window._dynarch_popupCalendar = null; +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "About the calendar"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Go Today"; +Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Select date"; +Calendar._TT["DRAG_TO_MOVE"] = "Drag to move"; +Calendar._TT["PART_TODAY"] = " (today)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Display %s first"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Close"; +Calendar._TT["TODAY"] = "Today"; +Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; +/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/ + * --------------------------------------------------------------------------- + * + * The DHTML Calendar + * + * Details and latest version at: + * http://dynarch.com/mishoo/calendar.epl + * + * This script is distributed under the GNU Lesser General Public License. + * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html + * + * This file defines helper functions for setting up the calendar. They are + * intended to help non-programmers get a working calendar on their site + * quickly. This script should not be seen as part of the calendar. It just + * shows you what one can do with the calendar, while in the same time + * providing a quick and simple method for setting it up. If you need + * exhaustive customization of the calendar creation process feel free to + * modify this code to suit your needs (this is recommended and much better + * than modifying calendar.js itself). + */ + +// $Id: calendar-setup.js,v 1.1 2005/09/06 08:38:43 ben Exp $ + +/** + * This function "patches" an input field (or other element) to use a calendar + * widget for date selection. + * + * The "params" is a single object that can have the following properties: + * + * prop. name | description + * ------------------------------------------------------------------------------------------------- + * inputField | the ID of an input field to store the date + * displayArea | the ID of a DIV or other element to show the date + * button | ID of a button or other element that will trigger the calendar + * eventName | event that will trigger the calendar, without the "on" prefix (default: "click") + * ifFormat | date format that will be stored in the input field + * daFormat | the date format that will be used to display the date in displayArea + * singleClick | (true/false) wether the calendar is in single click mode or not (default: true) + * firstDay | numeric: 0 to 6. "0" means display Sunday first, "1" means display Monday first, etc. + * align | alignment (default: "Br"); if you don't know what's this see the calendar documentation + * range | array with 2 elements. Default: [1900, 2999] -- the range of years available + * weekNumbers | (true/false) if it's true (default) the calendar will display week numbers + * flat | null or element ID; if not null the calendar will be a flat calendar having the parent with the given ID + * flatCallback | function that receives a JS Date object and returns an URL to point the browser to (for flat calendar) + * disableFunc | function that receives a JS Date object and should return true if that date has to be disabled in the calendar + * onSelect | function that gets called when a date is selected. You don't _have_ to supply this (the default is generally okay) + * onClose | function that gets called when the calendar is closed. [default] + * onUpdate | function that gets called after the date is updated in the input field. Receives a reference to the calendar. + * date | the date that the calendar will be initially displayed to + * showsTime | default: false; if true the calendar will include a time selector + * timeFormat | the time format; can be "12" or "24", default is "12" + * electric | if true (default) then given fields/date areas are updated for each move; otherwise they're updated only on close + * step | configures the step of the years in drop-down boxes; default: 2 + * position | configures the calendar absolute position; default: null + * cache | if "true" (but default: "false") it will reuse the same calendar object, where possible + * showOthers | if "true" (but default: "false") it will show days from other months too + * + * None of them is required, they all have default values. However, if you + * pass none of "inputField", "displayArea" or "button" you'll get a warning + * saying "nothing to setup". + */ +Calendar.setup = function (params) { + function param_default(pname, def) { if (typeof params[pname] == "undefined") { params[pname] = def; } }; + + param_default("inputField", null); + param_default("displayArea", null); + param_default("button", null); + param_default("eventName", "click"); + param_default("ifFormat", "%Y/%m/%d"); + param_default("daFormat", "%Y/%m/%d"); + param_default("singleClick", true); + param_default("disableFunc", null); + param_default("dateStatusFunc", params["disableFunc"]); // takes precedence if both are defined + param_default("dateText", null); + param_default("firstDay", null); + param_default("align", "Br"); + param_default("range", [1900, 2999]); + param_default("weekNumbers", true); + param_default("flat", null); + param_default("flatCallback", null); + param_default("onSelect", null); + param_default("onClose", null); + param_default("onUpdate", null); + param_default("date", null); + param_default("showsTime", false); + param_default("timeFormat", "24"); + param_default("electric", true); + param_default("step", 2); + param_default("position", null); + param_default("cache", false); + param_default("showOthers", false); + param_default("multiple", null); + + var tmp = ["inputField", "displayArea", "button"]; + for (var i in tmp) { + if (typeof params[tmp[i]] == "string") { + params[tmp[i]] = document.getElementById(params[tmp[i]]); + } + } + if (!(params.flat || params.multiple || params.inputField || params.displayArea || params.button)) { + alert("Calendar.setup:\n Nothing to setup (no fields found). Please check your code"); + return false; + } + + function onSelect(cal) { + var p = cal.params; + var update = (cal.dateClicked || p.electric); + if (update && p.inputField) { + p.inputField.value = cal.date.print(p.ifFormat); + if (typeof p.inputField.onchange == "function") + p.inputField.onchange(); + } + if (update && p.displayArea) + p.displayArea.innerHTML = cal.date.print(p.daFormat); + if (update && typeof p.onUpdate == "function") + p.onUpdate(cal); + if (update && p.flat) { + if (typeof p.flatCallback == "function") + p.flatCallback(cal); + } + if (update && p.singleClick && cal.dateClicked) + cal.callCloseHandler(); + }; + + if (params.flat != null) { + if (typeof params.flat == "string") + params.flat = document.getElementById(params.flat); + if (!params.flat) { + alert("Calendar.setup:\n Flat specified but can't find parent."); + return false; + } + var cal = new Calendar(params.firstDay, params.date, params.onSelect || onSelect); + cal.showsOtherMonths = params.showOthers; + cal.showsTime = params.showsTime; + cal.time24 = (params.timeFormat == "24"); + cal.params = params; + cal.weekNumbers = params.weekNumbers; + cal.setRange(params.range[0], params.range[1]); + cal.setDateStatusHandler(params.dateStatusFunc); + cal.getDateText = params.dateText; + if (params.ifFormat) { + cal.setDateFormat(params.ifFormat); + } + if (params.inputField && typeof params.inputField.value == "string") { + cal.parseDate(params.inputField.value); + } + cal.create(params.flat); + cal.show(); + return false; + } + + var triggerEl = params.button || params.displayArea || params.inputField; + triggerEl["on" + params.eventName] = function() { + var dateEl = params.inputField || params.displayArea; + var dateFmt = params.inputField ? params.ifFormat : params.daFormat; + var mustCreate = false; + var cal = window.calendar; + if (dateEl) + params.date = Date.parseDate(dateEl.value || dateEl.innerHTML, dateFmt); + if (!(cal && params.cache)) { + window.calendar = cal = new Calendar(params.firstDay, + params.date, + params.onSelect || onSelect, + params.onClose || function(cal) { cal.hide(); }); + cal.showsTime = params.showsTime; + cal.time24 = (params.timeFormat == "24"); + cal.weekNumbers = params.weekNumbers; + mustCreate = true; + } else { + if (params.date) + cal.setDate(params.date); + cal.hide(); + } + if (params.multiple) { + cal.multiple = {}; + for (var i = params.multiple.length; --i >= 0;) { + var d = params.multiple[i]; + var ds = d.print("%Y%m%d"); + cal.multiple[ds] = d; + } + } + cal.showsOtherMonths = params.showOthers; + cal.yearStep = params.step; + cal.setRange(params.range[0], params.range[1]); + cal.params = params; + cal.setDateStatusHandler(params.dateStatusFunc); + cal.getDateText = params.dateText; + cal.setDateFormat(dateFmt); + if (mustCreate) + cal.create(); + cal.refresh(); + if (!params.position) + cal.showAtElement(params.button || params.displayArea || params.inputField, params.align); + else + cal.showAt(params.position[0], params.position[1]); + return false; + }; + + return cal; +}; diff --git a/media/favicon.ico b/media/favicon.ico new file mode 100644 index 0000000..3a6d2ab Binary files /dev/null and b/media/favicon.ico differ diff --git a/media/forms/_library/cmxform.css b/media/forms/_library/cmxform.css new file mode 100644 index 0000000..ff36286 --- /dev/null +++ b/media/forms/_library/cmxform.css @@ -0,0 +1,57 @@ +/********************************** + +Use: cmxform template +Author: Nick Rigby + +***********************************/ + +form.cmxform fieldset { margin-bottom: 10px; } + +form.cmxform legend { + padding: 0 2px; + font-weight: bold; + _margin: 0 -7px; /* IE Win */ + } + +form.cmxform label { + display: inline-block; + line-height: 1.8; + vertical-align: top; + } + +form.cmxform fieldset ol { + margin: 0; + padding: 0; + } + +form.cmxform fieldset li { + list-style: none; + padding: 5px; + margin: 0; + } + +form.cmxform fieldset fieldset { + border: none; + margin: 3px 0 0; + } + +form.cmxform fieldset fieldset legend { + padding: 0 0 5px; + font-weight: normal; + } + +form.cmxform fieldset fieldset label { + display: block; + width: auto; + } + +form.cmxform em { + font-weight: bold; + font-style: normal; + color: #f00; + } + +form.cmxform label { width: 120px; } /* Width of labels */ +form.cmxform fieldset fieldset label { margin-left: 123px; } /* Width plus 3 (html space) */ + +/*\*//*/ form.cmxform legend { display: inline-block; } /* IE Mac legend fix */ \ No newline at end of file diff --git a/media/forms/cmxform.css b/media/forms/cmxform.css new file mode 100644 index 0000000..f713714 --- /dev/null +++ b/media/forms/cmxform.css @@ -0,0 +1,30 @@ +/********************************** + +Name: cmxform Styles +Author: Nick Rigby + +***********************************/ + +form.cmxform { + width: 370px; + font-size: 1.1em; + color: #333; + } + +form.cmxform legend { padding-left: 0; } + +form.cmxform legend, +form.cmxform label { color: #333; } + +form.cmxform fieldset { + border: none; + border-top: 1px solid #C9DCA6; + background: url(../images/cmxform-fieldset.gif) left bottom repeat-x; + } + +form.cmxform fieldset fieldset { background: none; } + +form.cmxform fieldset li { + padding: 5px 10px 7px; + background: url(../images/cmxform-divider.gif) left bottom repeat-x; + } \ No newline at end of file diff --git a/media/forms/cmxform.js b/media/forms/cmxform.js new file mode 100644 index 0000000..0b7973d --- /dev/null +++ b/media/forms/cmxform.js @@ -0,0 +1,22 @@ +if( document.addEventListener ) document.addEventListener( 'DOMContentLoaded', cmxform, false ); + +function cmxform(){ + // Hide forms + $( 'form.cmxform' ).hide().end(); + + // Processing + $( 'form.cmxform' ).find( 'li/label' ).not( '.nocmx' ).each( function( i ){ + var labelContent = this.innerHTML; + var labelWidth = document.defaultView.getComputedStyle( this, '' ).getPropertyValue( 'width' ); + var labelSpan = document.createElement( 'span' ); + labelSpan.style.display = 'block'; + labelSpan.style.width = labelWidth; + labelSpan.innerHTML = labelContent; + this.style.display = '-moz-inline-box'; + this.innerHTML = null; + this.appendChild( labelSpan ); + } ).end(); + + // Show forms + $( 'form.cmxform' ).show().end(); +} \ No newline at end of file diff --git a/media/forms/core.css b/media/forms/core.css new file mode 100644 index 0000000..bdeac7a --- /dev/null +++ b/media/forms/core.css @@ -0,0 +1,22 @@ +/********************************** + +Use: Core Styles +Author: Nick Rigby + +***********************************/ + +body { + padding: 0 10px; + font: normal 62.5% "Lucida Grande", Helvetica, Verdana, Arial; + } + +p { margin: 10px 0; } + +.sr { + position: absolute; + left: -9999em; + top: 0; + width: 1px; + height: 1px; + overflow: hidden; + } \ No newline at end of file diff --git a/media/forms/images/cmxform-divider.gif b/media/forms/images/cmxform-divider.gif new file mode 100644 index 0000000..718a977 Binary files /dev/null and b/media/forms/images/cmxform-divider.gif differ diff --git a/media/forms/images/cmxform-fieldset.gif b/media/forms/images/cmxform-fieldset.gif new file mode 100644 index 0000000..0590c89 Binary files /dev/null and b/media/forms/images/cmxform-fieldset.gif differ diff --git a/media/forms/reset.css b/media/forms/reset.css new file mode 100644 index 0000000..c557f24 --- /dev/null +++ b/media/forms/reset.css @@ -0,0 +1,62 @@ +/********************************** + +Use: Reset Styles for all browsers +Author: Nick Rigby + +***********************************/ + +body, p, blockquote { + margin: 0; + padding: 0; + } + +a img, iframe { border: none; } + +/* Headers +------------------------------*/ + +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + font-size: 100%; + } + +/* Lists +------------------------------*/ + +ul, ol, dl, li, dt, dd { + margin: 0; + padding: 0; + } + +/* Links +------------------------------*/ + +a, a:link {} +a:visited {} +a:hover {} +a:active {} + +/* Forms +------------------------------*/ + +form, fieldset { + margin: 0; + padding: 0; + } + +fieldset { border: 1px solid #000; } + +legend { + padding: 0; + color: #000; + } + +input, textarea, select { + margin: 0; + padding: 1px; + font-size: 100%; + font-family: inherit; + } + +select { padding: 0; } \ No newline at end of file diff --git a/media/forms/screen.css b/media/forms/screen.css new file mode 100644 index 0000000..d21a36a --- /dev/null +++ b/media/forms/screen.css @@ -0,0 +1,16 @@ +/********************************** + +Use: Main Screen Import +Author: Nick Rigby + +***********************************/ + +@import "reset.css"; +@import "core.css"; + +@import "_library/cmxform.css"; +@import "cmxform.css"; + +/* IE5 Macintosh \*//*/ +@import "_ie/mac/5.css"; +/**/ \ No newline at end of file diff --git a/media/gnu-head-tiny.jpg b/media/gnu-head-tiny.jpg new file mode 100644 index 0000000..441be50 Binary files /dev/null and b/media/gnu-head-tiny.jpg differ diff --git a/media/jquery.js b/media/jquery.js new file mode 100644 index 0000000..cd439da --- /dev/null +++ b/media/jquery.js @@ -0,0 +1,14 @@ +/* + * jQuery - Current + * http://jquery.com/ + * + * To use, download this file to your server, save as jquery.js, + * and add this HTML into the ... of your web page: + * + * + * Copyright (c) 2006 John Resig + * Licensed under the MIT License: + * http://www.opensource.org/licenses/mit-license.php + */ +/* Built Fri May 12 13:01:23 2006 */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('7 $(a,c){8 $a=a||$.14||R;8 $c=c&&c.$4k&&c.1l(0)||c;l(1O 4g!="2r"){l($a.N==1g){8 S=I 1i("[^a-41-6z-6y-]");l(!S.3Z($a)){$c=$c&&$c.2L||R;l($c.2t($a).q==0){8 1m=$c.25($a);l(1m!=C)k 1m}}}H l($a.N==36){k $.1w($a,7(b){l(b.N==1g)k R.25(b);k b})}}8 T={B:$.2d($a,$c),$4k:"$6x: 29 $",1E:7(){k 6.1l().q},1l:7(i){k i==C?6.B:6.B[i]},E:7(f){D(8 i=0;i<6.1E();i++)$.1f(6.1l(i),f,[i]);k 6},3e:7(a,b){k 6.E(7(){l(b==C)D(8 j 1d a)$.W(6,j,a[j]);H $.W(6,a,b)})},3f:7(h){k h==C&&6.1E()?6.1l(0).1Z:6.3e("1Z",h)},2J:7(h){k h==C&&6.1E()?6.1l(0).2R:6.3e("2R",h)},1n:7(a,b){k a.N!=1g||b?6.E(7(){l(!b)D(8 j 1d a)$.W(6.L,j,a[j]);H $.W(6.L,a,b)}):$.1n(6.1l(0),a)},2s:7(){k 6.E(7(){8 d=$.19(6,"V");l(d=="1z"||d==\'\')$(6).1v();H $(6).1u()})},1v:7(a){k 6.E(7(){6.L.V=6.$$2k?6.$$2k:\'\';l($.19(6,"V")=="1z")6.L.V=\'33\'})},1u:7(a){k 6.E(7(){6.$$2k=$.19(6,"V");l(6.$$2k=="1z")6.$$2k=\'33\';6.L.V=\'1z\'})},6w:7(c){k 6.E(7(){l($.2b(6,c))k;6.1j+=(6.1j.q>0?" ":"")+c})},6v:7(c){k 6.E(7(){6.1j=c==C?\'\':6.1j.1x(I 1i(\'(^|\\\\s*\\\\b[^-])\'+c+\'($|\\\\b(?=[^-]))\',\'g\'),\'\')})},6u:7(c){k 6.E(7(){l($.2b(6,c))6.1j=6.1j.1x(I 1i(\'(\\\\s*\\\\b[^-])\'+c+\'($|\\\\b(?=[^-]))\',\'g\'),\'\');H 6.1j+=(6.1j.q>0?" ":"")+c})},6t:7(){6.E(7(){6.U.4h(6)});6.B=[];k 6},6s:7(){8 a=$.1X(1M);k 6.E(7(){8 b=a[0].2j(Q);6.U.2M(b,6);1G(b.1W)b=b.1W;b.4j(6)})},4i:7(){8 1D=6.1E()>1;8 a=$.1X(1M);k 6.E(7(){D(8 i=0;i1;8 a=$.1X(1M);k 6.E(7(){D(8 i=a.q-1;i>=0;i--)6.2M(1D?a[i].2j(Q):a[i],6.1W)})},6p:7(){8 1D=6.1E()>1;8 a=$.1X(1M);k 6.E(7(){D(8 i=0;i1;8 a=$.1X(1M);k 6.E(7(){D(8 i=a.q-1;i>=0;i--)6.U.2M(1D?a[i].2j(Q):a[i],6.6n)})},45:7(){k 6.E(7(){1G(6.1W)6.4h(6.1W)})},21:7(t,f){k 6.E(7(){1F(6,t,f)})},3B:7(t,f){k 6.E(7(){35(6,t,f)})},3A:7(t){k 6.E(7(){2U(6,t)})},2Q:7(t){8 1Y=[],F=[];6.E(7(){1Y[1Y.q]=6;F=$.Y(F,$.2d(t,6))});6.1Y=1Y;6.B=F;k 6},6m:7(){6.B=6.1Y;k 6},46:7(a){6.B=$.1w(6.B,7(d){k d.U});l(a)6.B=$.15(a,6.B).r;k 6},38:7(a){6.B=$.1w(6.B,$.38);l(a)6.B=$.15(a,6.B).r;k 6},6l:7(a){6.B=$.1w(6.B,$.12);l(a)6.B=$.15(a,6.B).r;k 6},15:7(t){6.B=$.15(t,6.B).r;k 6},2G:7(t){6.B=t.N==1g?$.15(t,6.B,16).r:$.1S(6.B,7(a){k a!=t});k 6},6k:7(t){6.B=$.Y(6.B,t.N==1g?$.2d(t):t.N==36?t:[t]);k 6},6j:7(t){k $.15(t,6.B).r.q>0},6i:7(t){k!6.s(t)}};D(8 i 1d $.G){l(T[i]!=C)T["1I"+i]=T[i];T[i]=$.G[i]}l(1O 4g!="2r"&&$a.N!=1g){l($c)$a=T.1l();D(8 i 1d T){(7(j){2P{l($a[j]==C){$a[j]=7(){k $.1f(T,T[j],1M)}}}2N(e){}})(i)}k $a}k T}$.1f=7(o,f,a){a=a||[];l(f.1f)k f.1f(o,a);H{8 p=[];D(8 i=0;i m[3]-0",2h:"m[3] - 0 == i",69:"m[3] - 0 == i",3c:"i == 0",1e:"i == r.q - 1",49:"i % 2 == 0",48:"i % 2 == 1","3c-2f":"$.12(a,0).B","2h-2f":"(m[3] == \'49\'?$.12(a,m[3]).n % 2 == 0 :(m[3] == \'48\'?$.12(a,m[3]).n % 2 == 1:$.12(a,m[3]).B))","1e-2f":"$.12(a,0,Q).B","2h-1e-2f":"$.12(a,m[3],Q).B","3c-2g-u":"$.1T(a,0)","2h-2g-u":"$.1T(a,m[3])","1e-2g-u":"$.1T(a,0,Q)","2h-1e-2g-u":"$.1T(a,m[3],Q)","47-2g-u":"$.1T(a) == 1","47-2f":"$.12(a).q == 1",46:"a.1a.q > 0",45:"a.1a.q == 0",68:"a == ( a.44 ? a.44 : R ).2L",67:"(a.66 || a.1Z).O(m[3]) != -1",65:"(!a.u || a.u != \'1q\') && ($.19(a,\'V\') != \'1z\' && $.19(a,\'2e\') != \'1q\')",1q:"(a.u && a.u == \'1q\') || $.19(a,\'V\') == \'1z\' || $.19(a,\'2e\') == \'1q\'",3k:"a.3b == 16",3b:"a.3b",2T:"a.2T"},".":"$.2b(a,m[2])","@":{"=":"$.W(a,m[3]) == m[4]","!=":"$.W(a,m[3]) != m[4]","~=":"$.2b($.W(a,m[3]),m[4])","|=":"$.W(a,m[3]).O(m[4]) == 0","^=":"$.W(a,m[3]).O(m[4]) == 0","$=":"$.W(a,m[3]).1o( $.W(a,m[3]).q - m[4].q, m[4].q ) == m[4]","*=":"$.W(a,m[3]).O(m[4]) >= 0","":"m[3] == \'*\' ? a.64.q > 0 : $.W(a,m[3])"},"[":"$.2d(m[2],a).q > 0"};$.G={};$.2d=7(t,14){14=14||$.14||R;l(t.N!=1g)k[t];l(t.O("//")==0){14=14.2L;t=t.1o(2,t.q)}H l(t.O("/")==0){14=14.2L;t=t.1o(1,t.q);l(t.O(\'/\'))t=t.1o(t.O(\'/\'),t.q)}8 F=[14];8 1V=[];8 1e=C;1G(t.q>0&&1e!=t){8 r=[];1e=t;t=$.1L(t);8 S=I 1i("^//","i");t=t.1x(S,"");l(t.O(\'..\')==0||t.O(\'/..\')==0){l(t.O(\'/\')==0)t=t.1o(1,t.q);r=$.1w(F,7(a){k a.U});t=t.1o(2,t.q);t=$.1L(t)}H l(t.O(\'>\')==0||t.O(\'/\')==0){r=$.1w(F,7(a){k(a.1a.q>0?$.12(a.1W):C)});t=t.1o(1,t.q);t=$.1L(t)}H l(t.O(\'+\')==0){r=$.1w(F,7(a){k $.12(a).40});t=t.1o(1,t.q);t=$.1L(t)}H l(t.O(\'~\')==0){r=$.1w(F,7(a){8 r=[];8 s=$.12(a);l(s.n>0)D(8 i=s.n;i0&&t.5O(/^[:\\\\.#\\\\[a-41-Z\\\\*]/)){8 S=I 1i("^\\\\[ *@([a-2H-9\\\\(\\\\)1I-]+) *([~!\\\\|\\\\*$^=]*) *\'?\\"?([^\'\\"]*)\'?\\"? *\\\\]","i");8 m=S.1C(t);l(m!=C){m=[\'\',\'@\',m[2],m[1],m[3]]}H{8 S=I 1i("^(\\\\[) *([^\\\\]]*) *\\\\]","i");8 m=S.1C(t);l(m==C){8 S=I 1i("^(:)([a-2H-9\\\\*1I-]*)\\\\( *[\\"\']?([^ \\\\)\'\\"]*)[\'\\"]? *\\\\)","i");8 m=S.1C(t);l(m==C){8 S=I 1i("^([:\\\\.#]*)([a-2H-9\\\\*1I-]*)","i");8 m=S.1C(t)}}}t=t.1x(S,"");l(m[1]==":"&&m[2]=="2G")r=$.15(m[3],r,16).r;H{l($.g[m[1]].N==1g)8 f=$.g[m[1]];H l($.g[m[1]][m[2]])8 f=$.g[m[1]][m[2]];l(f!=C){2O("f = 7(a,i){k "+f+"}");r=g(r,f)}}}k{r:r,t:t}};$.38=7(a){8 b=[];8 c=a.U;1G(c!=C&&c!=R){b[b.q]=c;c=c.U}k b};$.1L=7(t){k t.1x(/^\\s+|\\s+$/g,\'\')};$.1T=7(a,n,e){8 t=$.1S($.12(a),7(b){k b.2B==a.2B});l(e)n=t.q-n-1;k n!=C?t[n]==a:t.q};$.12=7(a,n,e){8 u=[];8 2c=a.U.1a;D(8 i=0;i<2c.q;i++){l(2c[i].2C==1)u[u.q]=2c[i];l(2c[i]==a)u.n=u.q-1}l(e)n=u.q-n-1;u.B=(u[n]==a);u.5N=(u.n>0?u[u.n-1]:C);u.40=(u.n0)z.1u();H z.1v()};z.2w=7(a){z.2y(z.B(),z.B()+a)};z.3P=7(){3t(z.1r);z.1r=C};z.M=M.N==1g?R.25(M):M;8 y=z.M.L;z.3N=y.2z;y.2z="1q";z.o={3Q:"32",28:(1A&&1A.28)||2A,1H:(1A&&1A.1H)||1A};z.3I=7(f,2Z){8 t=(I 3K).3J();8 p=(t-z.s)/z.o.28;l(t>=z.o.28+z.s){z.1s=2Z;z.3P();3O(7(){y.2z=z.3N;l(y.18=="3M"||y.1y=="3M")z.27("1z");l(1t!="26"&&z.o.31){$.30(z.M,"18");$.30(z.M,"1y")}l(z.o.1H.N==20){z.M.$1I=z.o.1H;z.M.$1I()}},13)}H z.1s=((-3L.5m(p*3L.5l)/2)+0.5)*(2Z-f)+f;z.a()};z.2y=7(f,t){l(z.1r)k;6.1s=f;z.a();z.2x=z.B();z.s=(I 3K).3J();z.1r=3r(7(){z.3I(f,t)},13)}}J.G=["1v","1u","2s"];J.1t=["3H","3G","5k","5j"];D(8 i 1d J.1t){(7(){8 c=J.1t[i];J[c]=7(a,b){k I J(a,b,c.2W(),c)}})()}J.2u=7(a,b){8 o=I J(a,b,"26");o.B=7(){k 5i(o.M.L.26)};o.a=7(){8 e=o.M.L;l(o.1s==1)o.1s=0.5h;l(23.2X)e.15="5g(26="+o.1s*5f+")";e.26=o.1s};o.2x=o.1s=1;o.a();k o};J.2v=7(e,o){8 z=6;8 h=I J.3H(e,o);l(o)o.1H=C;8 w=I J.3G(e,o);7 c(a,b,c){k(!a||a==c||b==c)}D(8 i 1d J.G){(7(){8 j=J.G[i];z[j]=7(a,b){l(c(a,b,"18"))h[j]();l(c(a,b,"1y"))w[j]()}})()}z.2w=7(c,d){h.2w(c);w.2w(d)}};J.2Y=7(e,o){8 z=6;8 r=I J.2v(e,o);l(o)o.1H=C;8 p=I J.2u(e,o);D(8 i 1d J.G){(7(){8 j=J.G[i];z[j]=7(a,b){p[j]();r[j](a,b)}})()}};8 e=["5e","5d","5c","2n","5b","3F","5a","3q","59","58","57","56","55","54","3z","3x","53","3E","3D","3C","52","51","50","4Z","4Y","17"];D(8 i=0;i=0)?"4L.3p":"4K.3p")}}$.P=7(u,1k,X,F){8 P=I 2V();l(P){P.4J(u||"2m",1k,Q);l(X)P.4I(\'4H-4G\',\'4F/x-4E-4D-4C\');P.4B=7(){l(P.4A==4){l(F)F(P);$.3n($.2q(P))}};P.4z(X)}};$.2q=7(r,u){k r.4y("4x-u").O("P")>0||u=="P"?r.4w:r.3g};$.1l=7(1k,F,u){$.P("2m",1k,C,7(r){l(F)F($.2q(r,u))})};$.4v=7(1k,F){$.1l(1k,F,"P")};$.3o=7(1k,X,F,u){$.P("3h",1k,$.2l(X),7(r){l(F)F($.2q(r,u))})};$.4u=7(1k,X,F){$.3o(1k,X,F,"P")};$.G.4t=7(2o){$.22=$.Y($.22,6.B);k 6.21(\'3m\',2o)};$.22=[];$.3n=7(X){D(8 i=0;i<$.22.q;i++)2U($.22[i],\'3m\',[X])};$.G.4s=7(2o){k 6.E(7(){8 a={};$(6).2Q("2p:2T,1q,1N,4r[@4q],3l").15(":3k").E(7(){a[6.3j||6.2S||6.U.3j||6.U.2S]=6.2R});$.P(6.4p||"2m",6.4o||"",$.2l(a),2o)})};$.2l=7(a){8 s=[];D(8 i 1d a)s[s.q]=i+"="+4n(a[i]);k s.3i("&")};$.G.2n=7(a,o,f){l(a&&a.N==20)k 6.21("2n",a);8 t="2m";l(o&&o.N==20){f=o;o=C}l(o!=C){o=$.2l(o);t="3h"}8 T=6;$.P(t,a,o,7(h){8 h=h.3g;T.3f(h).2Q("4m").E(7(){2P{2O(6.1N||6.4l||6.1Z)}2N(e){}});l(f)f(h)});k 6};',62,408,'||||||this|function|var||||||||||||return|if|||||length||||type|||||||cur|null|for|each|ret|fn|else|new|fx|element|style|el|constructor|indexOf|xml|true|document|re|self|parentNode|display|attr|data|merge|||event|sibling||context|filter|false|ready|height|getCSS|childNodes|events|case|in|last|apply|String|handlers|RegExp|className|url|get|obj|css|substr|els|hidden|timer|now|ty|hide|show|map|replace|width|none|op|handler|exec|clone|size|addEvent|while|onComplete|_|parseInt|speed|cleanSpaces|arguments|text|typeof|position|fixEvent|guid|grep|ofType|tag|done|firstChild|clean|old|innerHTML|Function|bind|ajaxHandles|window|preventDefault|getElementById|opacity|ss|duration||on|hasWord|tmp|Select|visibility|child|of|nth|div|cloneNode|oldblock|param|GET|load|callback|input|httpData|undefined|toggle|getElementsByTagName|Opacity|Resize|modify|io|custom|overflow|400|nodeName|nodeType|stopPropagation|returnValue|handleEvent|not|z0|fix|val|break|documentElement|insertBefore|catch|eval|try|find|value|id|checked|triggerEvent|XMLHttpRequest|toLowerCase|ActiveXObject|FadeSize|tt|setAuto|auto|px|block|tz|removeEvent|Array|getAll|parents|toUpperCase|oid|disabled|first|defaultView|set|html|responseText|POST|join|name|enabled|textarea|ajax|triggerAJAX|post|XMLHTTP|click|setInterval|addEventListener|clearInterval|hover|relatedTarget|toElement|mouseout|fromElement|mouseover|trigger|unbind|submit|select|reset|scroll|Width|Height|step|getTime|Date|Math|0px|oo|setTimeout|clear|unit|max|absolute|center|offsetHeight|offsetWidth|_show|_hide|delete|test|next|zA|setAttribute|shift|ownerDocument|empty|parent|only|odd|even|getComputedStyle|currentStyle|oWidth|oHeight|ov|od|Prototype|removeChild|append|appendChild|jquery|textContent|script|encodeURIComponent|action|method|selected|option|serialize|handleAJAX|postXML|getXML|responseXML|content|getResponseHeader|send|readyState|onreadystatechange|urlencoded|form|www|application|Type|Content|setRequestHeader|open|Msxml2|Microsoft|msie|userAgent|navigator|_toggle|onready|body|push|DOMContentLoaded|onhover|one|do|un|error|abort|keyup|keypress|keydown|change|mousemove|mouseleave|mouseenter|mouseup|mousedown|dblclick|unload|resize|contextmenu|focus|blur|100|alpha|9999|parseFloat|Top|Left|PI|cos|natural|top|left|relative|static|IMG|fadeIn|fadeOut|slideUp|slideDown|number|normal|75|xfast|200|fast|medium|600|slow|850|xslow|1200|crawl|nodeValue|cancelBubble|location|prev|match|getAttribute|ig|cssFloat|float|class|cssText|htmlFor|default|file|password|image|button|checkbox|radio|switch|attributes|visible|innerText|contains|root|eq|gt|lt|toString|createTextNode|createElement|getPropertyValue|clientWidth|clientHeight|isNot|is|add|siblings|end|nextSibling|after|before|prepend|appendTo|wrap|remove|toggleClass|removeClass|addClass|Rev|9_|Z0'.split('|'),0,{})) diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 0000000..b2b6d86 Binary files /dev/null and b/media/logo.png differ diff --git a/media/mailman-large.jpg b/media/mailman-large.jpg new file mode 100644 index 0000000..e184f3c Binary files /dev/null and b/media/mailman-large.jpg differ diff --git a/media/mailman.jpg b/media/mailman.jpg new file mode 100644 index 0000000..94a4c01 Binary files /dev/null and b/media/mailman.jpg differ diff --git a/media/mm-icon.png b/media/mm-icon.png new file mode 100644 index 0000000..42c3cf7 Binary files /dev/null and b/media/mm-icon.png differ diff --git a/media/rss.png b/media/rss.png new file mode 100644 index 0000000..c916459 Binary files /dev/null and b/media/rss.png differ diff --git a/media/tab.png b/media/tab.png new file mode 100644 index 0000000..56d2e61 Binary files /dev/null and b/media/tab.png differ diff --git a/media/title.png b/media/title.png new file mode 100644 index 0000000..e16971a Binary files /dev/null and b/media/title.png differ diff --git a/media/title_back.png b/media/title_back.png new file mode 100644 index 0000000..95c98d7 Binary files /dev/null and b/media/title_back.png differ diff --git a/news/__init__.py b/news/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/news/models.py b/news/models.py new file mode 100644 index 0000000..acd9667 --- /dev/null +++ b/news/models.py @@ -0,0 +1,19 @@ +from django.db import models +from django.contrib.auth.models import User +import re +from archlinux.utils import Stripper + +class News(models.Model): + id = models.AutoField(primary_key=True) + author = models.ForeignKey(User) + postdate = models.DateField(auto_now_add=True) + title = models.CharField(maxlength=255) + content = models.TextField() + class Meta: + db_table = 'news' + verbose_name_plural = 'news' + get_latest_by = 'postdate' + ordering = ['-postdate', '-id'] + + def get_absolute_url(self): + return '/news/%i/' % self.id diff --git a/news/views.py b/news/views.py new file mode 100644 index 0000000..29f2044 --- /dev/null +++ b/news/views.py @@ -0,0 +1,82 @@ +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.shortcuts import get_object_or_404 +from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth.models import User +from django import forms +from archlinux.utils import render_template +from archlinux.news.models import News +from datetime import date + +def view(request, newsid): + news = get_object_or_404(News, id=newsid) + return render_template('news/view.html', request, {'news':news}) + +def list(request): + news = News.objects.order_by('-postdate', '-id') + return render_template('news/list.html', request, {'news':news}) + +@user_passes_test(lambda u: u.has_perm('news.add_news')) +def add(request): + try: + m = User.objects.get(username=request.user.username) + except User.DoesNotExist: + return render_template('error_page.html', request, + {'errmsg': 'Cannot find a maintainer record for you! No posting allowed.'}) + + manipulator = News.AddManipulator() + if request.POST: + data = request.POST.copy() + # add in the author ID + data['author'] = m.id + errors = manipulator.get_validation_errors(data) + if not errors: + manipulator.do_html2python(data) + manipulator.save(data) + return HttpResponseRedirect('/news/') + else: + errors = {} + data = {} + + form = forms.FormWrapper(manipulator, data, errors) + return render_template('news/add.html', request, {'form': form}) + +@user_passes_test(lambda u: u.has_perm('news.delete_news')) +def delete(request, newsid): + news = get_object_or_404(News, id=newsid) + #if news.author.id != request.user.id: + # return render_template('error_page.html', request, {'errmsg': 'You do not own this news item'}) + if request.POST: + news.delete() + return HttpResponseRedirect('/news/') + return render_template('news/delete.html', request) + +@user_passes_test(lambda u: u.has_perm('news.change_news')) +def edit(request, newsid): + try: + m = User.objects.get(username=request.user.username) + except User.DoesNotExist: + return render_template('error_page.html', request, + {'errmsg': 'Cannot find a maintainer record for you! No posting allowed.'}) + try: + manipulator = News.ChangeManipulator(newsid) + except News.DoesNotExist: + raise Http404 + + news = manipulator.original_object +# if news.author != m: +# return render_template('error_page.html', request, {'errmsg': 'You do not own this news item'}) + if request.POST: + data = request.POST.copy() + # add in the author ID + data['author'] = news.author.id + errors = manipulator.get_validation_errors(data) + if not errors: + manipulator.do_html2python(data) + manipulator.save(data) + return HttpResponseRedirect('/news/') + else: + errors = {} + data = news.__dict__ + + form = forms.FormWrapper(manipulator, data, errors) + return render_template('news/add.html', request, {'form': form, 'news':news}) diff --git a/packages/__init__.py b/packages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/models.py b/packages/models.py new file mode 100644 index 0000000..c1b6c84 --- /dev/null +++ b/packages/models.py @@ -0,0 +1,91 @@ +from django.db import models +from django.contrib.auth.models import User +import re + +class PackageManager(models.Manager): + def get_flag_stats(self): + results = [] + # first the orphans + unflagged = self.filter(maintainer=0).count() + flagged = self.filter(maintainer=0).filter(needupdate=True).count() + results.append((User(id=0,first_name='Orphans'), unflagged, flagged)) + # now the rest + for maint in User.objects.all().order_by('first_name'): + unflagged = self.filter(maintainer=maint.id).count() + flagged = self.filter(maintainer=maint.id).filter(needupdate=True).count() + results.append((maint, unflagged, flagged)) + return results + +class Category(models.Model): + id = models.AutoField(primary_key=True) + category = models.CharField(maxlength=255) + class Meta: + db_table = 'categories' + verbose_name_plural = 'categories' + +class Repo(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(maxlength=255) + class Meta: + db_table = 'repos' + def last_update(self): + try: + latest = Package.objects.filter(repo__name__exact=self.name).order_by('-last_update')[0] + return latest.last_update + except IndexError: + return "N/A" + +class Package(models.Model): + id = models.AutoField(primary_key=True) + repo = models.ForeignKey(Repo) + maintainer = models.ForeignKey(User) + category = models.ForeignKey(Category) + needupdate = models.BooleanField(default=False) + pkgname = models.CharField(maxlength=255) + pkgver = models.CharField(maxlength=255) + pkgrel = models.CharField(maxlength=255) + pkgdesc = models.CharField(maxlength=255) + url = models.URLField() + sources = models.TextField() + depends = models.TextField() + last_update = models.DateTimeField(null=True, blank=True) + objects = PackageManager() + class Meta: + db_table = 'packages' + get_latest_by = 'last_update' + + def get_absolute_url(self): + return '/packages/%i/' % self.id + + def depends_urlize(self): + urls = '' + for dep in self.depends.split(' '): + # shave off any version qualifiers + nameonly = re.match(r"([a-z0-9-]+)", dep).group(1) + try: + p = Package.objects.filter(pkgname=nameonly)[0] + except IndexError: + # couldn't find a package in the DB -- it might be a virtual depend + urls = urls + '
  1. ' + dep + '
  2. ' + continue + url = '
    ' + urls = urls + url + return urls + + def sources_urlize(self): + urls = '' + for source in self.sources.split(' '): + if re.search('://', source): + url = '
  3. ' + source + '
  4. ' + else: + url = '
  5. ' + source + '
  6. ' + urls = urls + url + return urls + +class PackageFile(models.Model): + id = models.AutoField(primary_key=True) + pkg = models.ForeignKey(Package) + path = models.CharField(maxlength=255) + class Meta: + db_table = 'packages_files' + diff --git a/packages/templatetags/__init__.py b/packages/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/templatetags/package_extras.py b/packages/templatetags/package_extras.py new file mode 100644 index 0000000..8b55221 --- /dev/null +++ b/packages/templatetags/package_extras.py @@ -0,0 +1,28 @@ +from django import template + +register = template.Library() + +class BuildQueryStringNode(template.Node): + def __init__(self, sortfield): + self.sortfield = sortfield + def render(self, context): + qs = context['querystring'].copy() + if qs.has_key('sort') and qs['sort'] == self.sortfield: + qs['sort'] = '-' + self.sortfield + else: + qs['sort'] = self.sortfield + return '?' + qs.urlencode() + +@register.tag(name='buildsortqs') +def do_buildsortqs(parser, token): + try: + tagname, sortfield = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires a single argument" % tagname + if not (sortfield[0] == sortfield[-1] and sortfield[0] in ('"', "'")): + raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tagname + return BuildQueryStringNode(sortfield[1:-1]) + +@register.filter(name='space2br') +def space2br(value): + return value.replace(' ', '
    ') diff --git a/packages/views.py b/packages/views.py new file mode 100644 index 0000000..f9cecf1 --- /dev/null +++ b/packages/views.py @@ -0,0 +1,172 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.core.mail import send_mail +from django.template import Context, loader +from django.core import validators +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from archlinux.utils import validate, render_template +from datetime import datetime +from archlinux.packages.models import Package, PackageFile, Repo, Category + +def update(request): + if request.POST.has_key('adopt'): + mode = 'adopt' + message = 'Adoption was successful' + if request.POST.has_key('disown'): + mode = 'disown' + message = 'Disown was successful' + try: + maint = User.objects.get(username=request.user.username) + except User.DoesNotExist: + return render_template('error_page.html', request, {'errmsg':'No maintainer record found! Are you a maintainer?'}) + ids = request.POST.getlist('pkgid') + for id in ids: + pkg = Package.objects.get(id=id) + if mode == 'adopt' and pkg.maintainer_id == 0: + pkg.maintainer = maint + elif mode == 'disown' and pkg.maintainer == maint: + pkg.maintainer_id = 0 + else: + message = "You are not the current maintainer" + pkg.save() + return render_template('status_page.html', request, {'message':message}) + +def details(request, pkgid=0, name='', repo=''): + if pkgid == 0: + p = Package.objects.filter(pkgname=name) + if repo: p = p.filter(repo__name__exact=repo) + # if more then one result, send to the search view + if len(p) > 1: return search(request, name) + if len(p) < 1: return render_template('error_page.html', request, + {'errmsg': 'No matching packages.'}) + pkgid = p[0].id + + pkg = get_object_or_404(Package, id=pkgid) + return render_template('packages/details.html', request, {'pkg':pkg}) + +def search(request, query=''): + if request.GET.has_key('q'): + # take the q GET var over the one passed on the URL + query = request.GET['q'].strip() + + # fetch the form vars + repo = request.GET.get('repo', 'all') + category = request.GET.get('category', 'all') + lastupdate = request.GET.get('lastupdate', '') + limit = int(request.GET.get('limit', '50')) + skip = int(request.GET.get('skip', '0')) + sort = request.GET.get('sort', '') + maint = request.GET.get('maint', 'all') + + # build the form lists + repos = Repo.objects.order_by('name') + cats = Category.objects.order_by('category') + # copy GET data over and add the lists + c = request.GET.copy() + c['repos'], c['categories'] = repos, cats + c['limit'], c['skip'] = limit, skip + c['lastupdate'] = lastupdate + c['sort'] = sort + # 'q' gets renamed to 'query', so it's not in GET + c['query'] = query + + # validate + errors = {} + validate(errors, 'Last Update', lastupdate, validators.isValidANSIDate, True, request) + validate(errors, 'Page Limit', str(limit), validators.isOnlyDigits, True, request) + validate(errors, 'Page Skip', str(skip), validators.isOnlyDigits, True, request) + if errors: + c['errors'] = errors + return render_template('packages/search.html', request, c) + + if query: + res1 = Package.objects.filter(pkgname__icontains=query) + res2 = Package.objects.filter(pkgdesc__icontains=query) + results = res1 | res2 + else: + results = Package.objects.all() + if repo != 'all': results = results.filter(repo__name__exact=repo) + if category != 'all': results = results.filter(category__category__exact=category) + if maint != 'all': results = results.filter(maintainer=maint) + if lastupdate: results = results.filter(last_update__gte=datetime(int(lastupdate[0:4]),int(lastupdate[5:7]),int(lastupdate[8:10]))) + # select_related() shouldn't be needed -- we're working around a Django bug + #results = results.select_related().order_by('repos.name', 'category', 'pkgname') + + # sort results + if sort == '': + results = results.order_by('repo', 'category', 'pkgname') + else: + # duplicate sort fields shouldn't hurt anything + results = results.order_by(sort, 'repo', 'category', 'pkgname') + + qs = request.GET.copy() + # build pagination urls + if results.count() > (skip + limit): + qs['skip'] = skip + limit + c['nextpage'] = '?' + qs.urlencode() + if skip > 0: + qs['skip'] = max(0, skip - limit) + c['prevpage'] = '?' + qs.urlencode() + # pass the querystring to the template so we can build sort queries + c['querystring'] = request.GET + + # if only there's only one result, pass right to the package details view + if results.count() == 1: return details(request, results[0].id) + # limit result set + if limit > 0: results = results[skip:(skip+limit)] + + c['results'] = results + return render_template('packages/search.html', request, c) + +def files(request, pkgid): + pkg = get_object_or_404(Package, id=pkgid) + files = PackageFile.objects.filter(pkg=pkgid) + return render_template('packages/files.html', request, {'pkg':pkg,'files':files}) + +def flaghelp(request): + return render_template('packages/flaghelp.html', request) + +def flag(request, pkgid): + pkg = get_object_or_404(Package, id=pkgid) + context = {'pkg': pkg} + if request.POST.has_key('confirmemail'): + email = request.POST['confirmemail'] + if request.POST.has_key('usermessage'): + message = request.POST['usermessage'] + else: + message = None + # validate + errors = {} + validate(errors, 'Email Address', email, validators.isValidEmail, False, request) + if errors: + context['errors'] = errors + return render_template('packages/flag.html', request, context) + + context['confirmemail'] = email + pkg.needupdate = 1 + pkg.save() + if pkg.maintainer_id > 0: + # send notification email to the maintainer + t = loader.get_template('packages/outofdate.txt') + c = Context({ + 'email': request.POST['confirmemail'], + 'message': message, + 'pkgname': pkg.pkgname, + 'weburl': 'http://www.archlinux.org/packages/' + str(pkg.id) + '/' + }) + send_mail('arch: Package [%s] marked out-of-date' % pkg.pkgname, + t.render(c), + 'Arch Website Notification ', + [pkg.maintainer.email], + fail_silently=True) + return render_template('packages/flag.html', request, context) + +@login_required +def unflag(request, pkgid): + pkg = get_object_or_404(Package, id=pkgid) + if pkg.maintainer.username != request.user.username: + return render_template('error_page.html', request, {'errmsg': 'You do not own this package.'}) + pkg.needupdate = 0 + pkg.save() + return HttpResponseRedirect('/packages/%d/' % (pkg.id)) diff --git a/public/__init__.py b/public/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/public/views.py b/public/views.py new file mode 100644 index 0000000..577efcb --- /dev/null +++ b/public/views.py @@ -0,0 +1,56 @@ +from django.http import HttpResponse +from archlinux.utils import render_template +from django.contrib.auth.models import User +from archlinux.packages.models import Package, Repo +from archlinux.news.models import News +from archlinux.settings import DATA_DIR +from archlinux.common.models import Donator, Mirror + +def index(request): + # get the most recent 10 news items + news = News.objects.order_by('-postdate', '-id')[:10] + pkgs = Package.objects.exclude(repo__name__exact='Testing').order_by('-last_update')[:15] + repos = Repo.objects.order_by('name') + return render_template('public/index.html', request, {'news_updates':news,'pkg_updates':pkgs,'repos':repos}) + +def about(request): + return render_template('public/about.html', request) + +def art(request): + return render_template('public/art.html', request) + +def cvs(request): + return render_template('public/cvs.html', request) + +def developers(request): + devs = User.objects.order_by('username') + return render_template('public/developers.html', request, {'devs':devs}) + +def donate(request): + donor_count = Donator.objects.count() + splitval = donor_count / 4 + slice1 = Donator.objects.all()[:splitval] + slice2 = Donator.objects.all()[(splitval):(splitval*2)] + slice3 = Donator.objects.all()[(splitval*2):(donor_count-splitval)] + slice4 = Donator.objects.all()[(donor_count-splitval):donor_count] + return render_template('public/donate.html', request, + {'slice1':slice1,'slice2':slice2,'slice3':slice3,'slice4':slice4}) + +def download(request): + mirrors = Mirror.objects.order_by('country', 'domain') + return render_template('public/download.html', request, {'mirrors':mirrors}) + +def irc(request): + return render_template('public/irc.html', request) + +def moreforums(request): + return render_template('public/moreforums.html', request) + +def press(request): + return render_template('public/press.html', request) + +def projects(request): + return render_template('public/projects.html', request) + +def denied(request): + return render_template('public/denied.html', request) diff --git a/scripts/daily_cleanup.py b/scripts/daily_cleanup.py new file mode 100644 index 0000000..98f997b --- /dev/null +++ b/scripts/daily_cleanup.py @@ -0,0 +1,14 @@ +from django.db import backend, connection, transaction +""" Daily cleanup file + This purges the session data that is old from the session table. +""" +def clean_up(): + # Clean up old database records + cursor = connection.cursor() + cursor.execute("DELETE FROM %s WHERE %s < NOW()" % \ + (backend.quote_name('django_session'), backend.quote_name('expire_date'))) + cursor.execute("OPTIMIZE TABLE %s" % backend.quote_name('django_session')) + transaction.commit_unless_managed() + +if __name__ == "__main__": + clean_up() diff --git a/scripts/djangoshell.sh b/scripts/djangoshell.sh new file mode 100755 index 0000000..457c882 --- /dev/null +++ b/scripts/djangoshell.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd /home/sites/archlinux/archlinux +export PYTHONPATH=/usr/local/django:/home/sites/archlinux:${PYTHONPATH} +export DJANGO_SETTINGS_MODULE=archlinux.settings +python manage.py $* + diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..3de4a89 --- /dev/null +++ b/settings.py @@ -0,0 +1,89 @@ +# Django settings for archlinux project. + +## Import local settings +from local_settings import * + +## Set the debug values +TEMPLATE_DEBUG = DEBUG + +# Set managers to admins +MANAGERS = ADMINS + +## Cache backend settings +if ENABLE_CACHE == True: + CACHE_BACKEND = 'file:///tmp/ALdjangocache?timeout=900' + CACHE_MIDDLEWARE_SECONDS = 900 + CACHE_MIDDLEWARE_KEY_PREFIX = 'arch' + CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True + +# Full path to the data directory +DATA_DIR = '%s/data' % DEPLOY_PATH + +# Local time zone for this installation. All choices can be found here: +# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +TIME_ZONE = 'US/Eastern' + +# Language code for this installation. All choices can be found here: +# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes +# http://blogs.law.harvard.edu/tech/stories/storyReader$15 +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/admin-media/' + +# URL to send users when they don't have sufficient privileges +BADPRIVS_URL = '/denied/' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + 'django.middleware.http.ConditionalGetMiddleware', +) + +# A bit of hackery to insert caching at the right spot +if ENABLE_CACHE == True: + MIDDLEWARE_CLASSES += ('django.middleware.cache.CacheMiddleware',) + +MIDDLEWARE_CLASSES += ( + "django.middleware.common.CommonMiddleware", + "django.middleware.doc.XViewMiddleware", +) + +ROOT_URLCONF = 'archlinux.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates". + # Always use forward slashes, even on Windows. + '%s/templates' % DEPLOY_PATH, +) + +# Set django's User stuff to use our profile model +# format is app.model +AUTH_PROFILE_MODULE = 'common.UserProfile' + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.admin', + 'archlinux.common', + 'archlinux.news', + 'archlinux.packages', + 'archlinux.public', + 'archlinux.todolists', + 'archlinux.devel', + 'archlinux.wiki' +) + diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..c853fef --- /dev/null +++ b/templates/403.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    403 - Access Forbidden

    + Sorry, the page you've requested is not available. +
    +{% endblock %} + diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..bc50f69 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    404 - Page Not Found

    + Sorry, the page you've requested does not exist. +
    +{% endblock %} + diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..9566fc2 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    500 - Internal Server Error

    + Something has gone horribly wrong. Back away slowly. +
    +{% endblock %} + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..507e8ab --- /dev/null +++ b/templates/base.html @@ -0,0 +1,84 @@ + + + + {% block title %}Arch Linux{% endblock %} + + + + + {% block head %} + {% endblock %} + + +
    +
    + +
    Arch Linux
    +
    +
    + {% if not user.is_anonymous %} + Logged in as {{ user.username }}. + Logout + {% endif %} +
    + + {% block ads %} + {% if not user.is_anonymous %} + + {% else %} +
    + + + {% endif %} +
    + {% endblock %} +
    +
    + {% block content %} +
    + {% block content_right %} + {% endblock %} +
    +
    + {% block content_left %} + {% endblock %} +
    + {% endblock %} +
    +
    + Copyright © 2002-2007, Judd Vinet <jvinet@zeroflux.org>

    + Arch Linux +
    + + diff --git a/templates/devel/index.html b/templates/devel/index.html new file mode 100644 index 0000000..2617de9 --- /dev/null +++ b/templates/devel/index.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} + +{% block content %} + {% if todos %} +
    +

    Package ToDo Lists

    + + + + + + + {% for todo in todos %} + + + + + + {% endfor %} +
    NameCreation DateDescription
    {{ todo.name }}{{ todo.date_added }}{{ todo.description }}
    +
    +

    + {% endif %} + +
    +

    Flagged Package Stats

    + + + + + + + {% for maint in stats %} + + + + + + {% endfor %} +
    Maintainer# Package# Flagged
    {{ maint.0.get_full_name }}{{ maint.1 }} packages{{ maint.2 }} packages
    +
    +

    + +
    +

    Package Maintenance

    +
    + + + + +
    + {% if maint %} + My Packages
    + {% endif %} + Orphan Packages
    +
    + Package Maintainer's Guide
    +
    + {% if pkgs %} +

    My Flagged Packages:

    +
      +
    • + Notify me when packages are flagged +     + +
    • +
    + + {% endif %} +
    +{% endblock %} diff --git a/templates/devel/profile.html b/templates/devel/profile.html new file mode 100644 index 0000000..6083735 --- /dev/null +++ b/templates/devel/profile.html @@ -0,0 +1,32 @@ +{% load validation %} +{% extends "base.html" %} + +{% block content %} +
    +

    Developer Profile

    + {% if errors %} + {% print_errors errors %} + {% endif %} +
    + + + + + + + + + + + + + + + + +
    Username:{{ user.username }}
    Email Address:
    New Password:
    Confirm Password:
    + +
    +
    +
    +{% endblock %} diff --git a/templates/error_page.html b/templates/error_page.html new file mode 100644 index 0000000..575ac41 --- /dev/null +++ b/templates/error_page.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    {{ errmsg }}

    +
    +{% endblock %} + diff --git a/templates/errors.html b/templates/errors.html new file mode 100644 index 0000000..84f98d7 --- /dev/null +++ b/templates/errors.html @@ -0,0 +1,5 @@ +
      +{% for err in errors %} +
    • {{ err.0 }}: {{ err.1 }}
    • +{% endfor %} +
    diff --git a/templates/feeds/news_description.html b/templates/feeds/news_description.html new file mode 100644 index 0000000..26e1cbb --- /dev/null +++ b/templates/feeds/news_description.html @@ -0,0 +1 @@ +{{ obj.content }} diff --git a/templates/feeds/news_title.html b/templates/feeds/news_title.html new file mode 100644 index 0000000..d355de5 --- /dev/null +++ b/templates/feeds/news_title.html @@ -0,0 +1 @@ +{{ obj.title }} diff --git a/templates/feeds/packages_description.html b/templates/feeds/packages_description.html new file mode 100644 index 0000000..6b9c47b --- /dev/null +++ b/templates/feeds/packages_description.html @@ -0,0 +1 @@ +{{ obj.pkgdesc }} diff --git a/templates/feeds/packages_title.html b/templates/feeds/packages_title.html new file mode 100644 index 0000000..a273162 --- /dev/null +++ b/templates/feeds/packages_title.html @@ -0,0 +1 @@ +{{ obj.pkgname }} {{ obj.pkgver }}-{{ obj.pkgrel }} diff --git a/templates/news/add.html b/templates/news/add.html new file mode 100644 index 0000000..702d7ab --- /dev/null +++ b/templates/news/add.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +
    + {% if news %} +

    Edit News

    + {% else %} +

    Add News

    + {% endif %} +
    + + + + + + + + + + +
    Title:{{ form.title }}
    Content:{{ form.content }}
    + +
    +
    +
    +{% endblock %} diff --git a/templates/news/delete.html b/templates/news/delete.html new file mode 100644 index 0000000..7ac5e25 --- /dev/null +++ b/templates/news/delete.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Confirm Delete

    +
    +
    + + + + + +
    Are You Sure?   
    +
    +
    +{% endblock %} diff --git a/templates/news/list.html b/templates/news/list.html new file mode 100644 index 0000000..e188e3c --- /dev/null +++ b/templates/news/list.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +
    + {% if perms.news.add_news %} + + {% endif %} +

    News Updates

    + + {% for item in news %} + + + + + + {% endfor %} +
    {{ item.postdate }}{{ item.title }} + {% comment %}{% if item.author %}{% ifequal user.username item.author.username %}{% endcomment %} + edit + delete + {% comment %}{% endifequal %}{% endif %}{% endcomment %} +
    +
    +{% endblock %} diff --git a/templates/news/view.html b/templates/news/view.html new file mode 100644 index 0000000..5d10045 --- /dev/null +++ b/templates/news/view.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
    +
    + {{ news.author.get_full_name }}
    + {{ news.postdate }} +
    +

    {{ news.title }}

    +

    + {{ news.content|linebreaks }} +
    +{% endblock %} diff --git a/templates/packages/details.html b/templates/packages/details.html new file mode 100644 index 0000000..65de079 --- /dev/null +++ b/templates/packages/details.html @@ -0,0 +1,77 @@ +{% load package_extras %} +{% extends "base.html" %} + +{% block content %} +
    +

    {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }}

    +
    +
      +
    • View CVS Entries
    • +
    • View File List
    • +
    • + {% if pkg.needupdate %} + This package has been flagged out-of-date + {% if not user.is_anonymous %}{% if pkg.maintainer %}{% ifequal user.username pkg.maintainer.username %} +
          Click here to unflag + {% endifequal %}{% endif %}{% endif %} + {% else %} + Flag Package Out-of-Date + (?) + {% endif %} +
    • + {% if not user.is_anonymous %} +
    •  
    • +
    • +
      + + + +
      +
    • + {% endif %} +
    +
    + + + + + + + + + + + + + + + + + + + + +
    Repository:{{ pkg.repo.name }}
    Category:{{ pkg.category.category }}
    Description:{{ pkg.pkgdesc }}
    URL:{{ pkg.url }}
    Maintainer:{% if pkg.maintainer %}{{ pkg.maintainer.get_full_name }}{% else %}None{% endif %}
    LastUpdated:{{ pkg.last_update|date:"Y-m-d" }}
    +
    + + + + +
    +
    +

    Dependencies:

    +
      + {{ pkg.depends_urlize }} +
    +
    +
    +
    +

    Sources:

    +
      + {{ pkg.sources_urlize }} +
    +
    +
    +
    +{% endblock %} + diff --git a/templates/packages/files.html b/templates/packages/files.html new file mode 100644 index 0000000..24f5581 --- /dev/null +++ b/templates/packages/files.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Viewing Files: {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }}

    + {% for file in files %} + {{ file.path }}
    + {% endfor %} +
    +{% endblock %} + diff --git a/templates/packages/flag.html b/templates/packages/flag.html new file mode 100644 index 0000000..215b6fa --- /dev/null +++ b/templates/packages/flag.html @@ -0,0 +1,26 @@ +{% load validation %} + +Flagging Packages + +{% if errors %} + {% print_errors errors %} +{% endif %} + +{% if confirmemail %} + Thank you. Maintainers have been notified. +{% else %} +
    + Please confirm your flag request.
    +
    + Email Address: (required)
    +
    +
    + Message to dev: (optional)
    +
    + +
    +{% endif %} +
    + + + diff --git a/templates/packages/flaghelp.html b/templates/packages/flaghelp.html new file mode 100644 index 0000000..09f7530 --- /dev/null +++ b/templates/packages/flaghelp.html @@ -0,0 +1,14 @@ + +Flagging Packages + + +If you notice that one of Arch's packages is out of date (ie, there is a newer +stable release available), then please notify us by using the Flag +button in the Package Details screen. This will notify the maintainer +responsible for that package so they can update it. +

    +Note: Please do not use this facility if the package is broken! +Use the bugtracker instead. +
    + + diff --git a/templates/packages/outofdate.txt b/templates/packages/outofdate.txt new file mode 100644 index 0000000..7b86360 --- /dev/null +++ b/templates/packages/outofdate.txt @@ -0,0 +1,13 @@ + +* Note: this is an automated message + +{{ email }} wants to notify you that the following package may be out +of date: + + {{ pkgname }} ({{ weburl }}) +{% if message %} +The user provided the following additional text: + +{{ message }} +{% endif %} + diff --git a/templates/packages/search.html b/templates/packages/search.html new file mode 100644 index 0000000..030289d --- /dev/null +++ b/templates/packages/search.html @@ -0,0 +1,120 @@ +{% load validation %} +{% load package_extras %} +{% extends "base.html" %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
    +

    Search Criteria

    + {% if errors %} + {% print_errors errors %} + {% endif %} +
    +
    + + + + + + + + + + +
    RepositoryCategoryKeywordsLast UpdatePer Page
    + + + + + + + + + + + + +
    +
    +
    +

    + + {% if results %} +
    + + + {% if not user.is_anonymous %} + + + {% endif %} + + + + + + + + {% for pkg in results %} + + {% if not user.is_anonymous %} + + {% endif %} + + + + {% if pkg.needupdate %} + + {% else %} + + {% endif %} + + + + {% endfor %} + + + + + + {% if not user.is_anonymous %} + + + + + + + + {% endif %} +
     RepoCategoryNameVersionDescriptionLast Updated
    {{ pkg.repo.name }}{{ pkg.category.category }}{{ pkg.pkgname }}{{ pkg.pkgver }}-{{ pkg.pkgrel }}{{ pkg.pkgver }}-{{ pkg.pkgrel }}{{ pkg.pkgdesc }}{{ pkg.last_update|date:"Y-m-d" }}
    {% if prevpage %}
    <<< Prev{% endif %}
     {% if nextpage %}
    Next >>>{% endif %}
      
    +
    + {% endif %} +{% endblock %} + diff --git a/templates/public/about.html b/templates/public/about.html new file mode 100644 index 0000000..0712b5d --- /dev/null +++ b/templates/public/about.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    About Arch Linux

    + +

    + Arch Linux is a general purpose linux distribution that can be molded to + do just about anything. It is fast, lightweight, flexible, and most of the + parts under the hood are quite simple to understand and tweak, which can + make it a good distro to "learn the ropes" on. We do not provide any + configuration helper utilities (ie, you won't find linuxconf in + here) so you will quickly become very proficient at configuring your system + from the shell commandline. +

    + +

    + Arch Linux uses i686-optimized packages which gives us improved + performance over some of our i386-optimized cousins. This means that Arch + Linux will only run on a Pentium II processor or higher. We try to stay + fairly bleeding edge, and typically have the latest stable versions of + software. +

    + +

    + Arch Linux uses the Pacman + package manager, which couples a simple binary package format with an + easy-to-use build system, allowing the users to easily manage and customize + their packages, whether they be official Arch packages or the user's own + homegrown ones. The repository system allows users to build and maintain + their own custom package repositories, which encourages community growth and + contribution. +

    + +

    + Pacman can keep a system up to date by synchronizing package lists with + the master server, making it a breeze for the security-conscious system + administrator to maintain. This server/client model also allows you to + download/install packages with a simple command, complete with all required + dependencies (similar to Debian's apt-get). +

    + +

    + Arch's official package set is fairly streamlined, but we supplement this + with a larger, more complete "extra" repository that contains a lot of the + stuff that never made it into our core package set. This repository is + constantly growing with the help of packages submitted from our strong + community. +

    + +

    + Arch Linux does not provide any official support, but you will find a lot + of helpful people on our IRC channel and on our user forums. Chances are that some other + Archer has had the same problem/question as you and it's already been + answered. Ask around! +

    + +

    + Arch Linux uses a "rolling release" system which works like this: We have + two versions of our core package set at any given time, Current and + Release. The Current repository always contains the latest and + greatest versions of packages. As soon as a package is updated it is part of + the Current repository, so this is the one to follow if you want to stay very + up to date. The Release repository follows the semi-regular snapshot + releases and does not update until the next snapshot/iso has been released. + For example, the Release repository will point to all packages on the 0.5 ISO + until we release 0.6; then it will point to 0.6 packages until 0.7 is + released. This is useful if you only want to update your system when a new + release is available. +

    + +

    + So, to sum up: Arch Linux is a workhorse distribution designed to fit the + needs of the competent linux user. We strive to make it both powerful and + easy to manage, making it an ideal distro for servers and workstations. Take + it in any direction you like. +

    +
    +

    +{% endblock %} + diff --git a/templates/public/art.html b/templates/public/art.html new file mode 100644 index 0000000..0fab30a --- /dev/null +++ b/templates/public/art.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block content %} +

    +
    +

    Arch Linux Logos

    +

    + + + + + + + + + + + + + + + + +

    SVG

    SVG

    SVG

    SVG

    SVG
    +
    +

    +{% endblock %} + diff --git a/templates/public/blank.html b/templates/public/blank.html new file mode 100644 index 0000000..d1e7b71 --- /dev/null +++ b/templates/public/blank.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Download Arch Linux

    +

    +
    +

    +{% endblock %} + diff --git a/templates/public/cvs.html b/templates/public/cvs.html new file mode 100644 index 0000000..715d00d --- /dev/null +++ b/templates/public/cvs.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block content %} +
    + You can access all of our PKGBUILD files from the + cvsweb interface. +
    +

    +
    +

    CVS Repositories

    +

    + Anonymous CVS access is also available. Use anonymous as the + username and password.

    +
      +
    1. + Set CVSROOT to the repository you wish to access:

      +
      +# export CVSROOT=:pserver:anonymous@cvs.archlinux.org:/home/cvs-core

      +OR

      +# export CVSROOT=:pserver:anonymous@cvs.archlinux.org:/home/cvs-extra

      +OR

      +# export CVSROOT=:pserver:anonymous@cvs.archlinux.org:/home/cvs-unstable

      +
      +

      +
    2. +
    3. + Login:

      +
      +# touch ~/.cvspass
      +# cvs login
      +Logging in to :pserver:anonymous@cvs.archlinux.org:2401/home/cvs-core
      +CVS password: anonymous
      +
      +

      +
    4. +
    5. + Check out the repository:

      +
      +# cvs -z3 co core

      +OR

      +# cvs -z3 co extra
      +
      +
    6. +
    +
    +

    +{% endblock %} + diff --git a/templates/public/denied.html b/templates/public/denied.html new file mode 100644 index 0000000..c59c0f7 --- /dev/null +++ b/templates/public/denied.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    + Sorry, you don't have sufficient privileges to perform this function. +

    +
    +

    +{% endblock %} + diff --git a/templates/public/developers.html b/templates/public/developers.html new file mode 100644 index 0000000..6b1fe8b --- /dev/null +++ b/templates/public/developers.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block content %} + +

    +{% endblock %} + diff --git a/templates/public/donate.html b/templates/public/donate.html new file mode 100644 index 0000000..bdb9ade --- /dev/null +++ b/templates/public/donate.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Donate to Arch Linux

    +

    + Arch Linux survives because of the tireless efforts of many people in + the community and the core development circle. None of us are paid for our + work, and we don't have the personal funds to sustain server costs ourselves. +

    + There are many ways to help Arch Linux. If technical development, + documentation, or support aren't your strong points, you could certainly + help us by dropping a few bucks our way. +

    + Many thanks! +

    +
    + +
    + + + + + + + + +
    +
    +

    +

    Past Donors

    + + + + + + + +
    + {% for donor in slice1 %} + {{ donor.name }}
    + {% endfor %} +
    + {% for donor in slice2 %} + {{ donor.name }}
    + {% endfor %} +
    + {% for donor in slice3 %} + {{ donor.name }}
    + {% endfor %} +
    + {% for donor in slice4 %} + {{ donor.name }}
    + {% endfor %} +
    +
    +

    +{% endblock %} + diff --git a/templates/public/download.html b/templates/public/download.html new file mode 100644 index 0000000..be840d3 --- /dev/null +++ b/templates/public/download.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Buy A CD

    + CDs are available for purchase from OSDisc.com. For each CD purchased, a + portion of the money goes to the Arch Linux Project.

    + +

    + +

    BitTorrent Download

    +

    +
    +

    Download with BitTorrent

    + If you can spare the bytes, please leave the BT client + open after your
    download is finished, so you can seed it back to others.
    +
    + + + + + + + + + + + + + + + +
     COREFTP Install
    i686: + + 2007.08-2 + + + + 2007.08-2 + +
    x86_64: + + 2007.08-2 + + + + 2007.08-2 + +
    +
    +

    + +

    HTTP/FTP Download

    +

    +
    +

    Download with HTTP/FTP

    + In addition to the BitTorrent links above, ISO images can also be downloaded
    via HTTP/FTP from the /iso/ sub-directory of mirror sites listed below.
    +

    +

    Mirror Sites

    + + {% for mirror in mirrors %} + + + + + {% endfor %} +
    {{ mirror.domain }}{{ mirror.country }}
    +
    +
    +

    +{% endblock %} + diff --git a/templates/public/index.html b/templates/public/index.html new file mode 100644 index 0000000..2e0d005 --- /dev/null +++ b/templates/public/index.html @@ -0,0 +1,174 @@ +{% extends "base.html" %} + +{% block head %} + + +{% endblock %} + +{% block content_left %} +
    +

    Welcome to Arch!

    +

    + You've reached the website for Arch Linux, a lightweight + and flexible linux distribution that tries to Keep It Simple. +

    + Currently we have official packages optimized for the i686 and x86-64 + architectures. We complement our official package sets with a + community-operated package repository + that grows in size and quality each and every day. +

    + Our strong community is diverse and helpful, and we pride ourselves on + the range of skillsets and uses for Arch that stem from it. Please + check out our forums and + mailing lists + to get your feet wet. Also glance through our wiki + if you want to learn more about Arch. +

    + Learn more... +

    +
    +

    +
    + RSS Feed +
    +

    Latest News

    +
    + {% for news in news_updates %} +
    + {{ news.postdate }} +

    {{ news.title }}

    +

    {{ news.content|striptags|truncatewords:60 }}

    +
    + {% endfor %} + More News... +

    +
    +{% endblock %} + +{% block content_right %} + +
    + + + + + + {% for pkg in pkg_updates %} + + + + + {% endfor %} + + + +

    Recent Updates

    RSS Feed
    {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }}{{ pkg.category.category }}

    More...
    +
    +
    +
    +

    Package Repositories

    + + {% for repo in repos %} + + + + + {% endfor %} + +
    +
    +
    +

    Releases

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    2007.08-2Don't Panic2007-10-07
    2007.08.1Don't Panic2007-09-10
    2007.08Don't Panic2007-08-05
    2007.05Duke2007-05-17
    0.8Voodoo2007-03-31
    0.7.2Gimmick2006-05-23
    0.7.1Noodle2006-01-05
    0.7Wombat2005-01-24
    0.6Widget2004-03-01
    0.5Nova2003-07-21
    0.4Dragon2002-12-18
    0.3Firefly2002-08-07
    0.2Vega2002-04-17
    0.1Homer2002-03-11
    +
    +
    +

    Documentation:

    + +

    Support Arch:

    + +

    Community Links:

    + +

    Development:

    + +{% endblock %} diff --git a/templates/public/index.html.bak b/templates/public/index.html.bak new file mode 100644 index 0000000..a41196d --- /dev/null +++ b/templates/public/index.html.bak @@ -0,0 +1,151 @@ +{% extends "base.html" %} + +{% block head %} + + +{% endblock %} + +{% block content_left %} +
    +

    Welcome to Arch!

    +

    + You've reached the website for Arch Linux, a lightweight + and flexible linux distribution that tries to Keep It Simple. +

    + Currently we have official packages optimized for the i686 and x86-64 + architectures. We complement our official package sets with a + community-operated package repository + that grows in size and quality each and every day. +

    + Our strong community is diverse and helpful, and we pride ourselves on + the range of skillsets and uses for Arch that stem from it. Please + check out our forums and + mailing lists + to get your feet wet. Also glance through our wiki + if you want to learn more about Arch. +

    + Learn more... +

    +
    +

    +
    + RSS Feed +
    +

    Latest News

    +
    + {% for news in news_updates %} + {{ news.postdate }} +

    {{ news.title }}

    +

    {{ news.content|striptags|truncatewords:60 }}



    + {% endfor %} +
    +{% endblock %} + +{% block content_right %} + +

    +
    +
    + RSS Feed +
    +

    Recent Updates

    + + {% for pkg in pkg_updates %} + + + + + {% endfor %} +
    {{ pkg.pkgname }} {{ pkg.pkgver }}-{{ pkg.pkgrel }}{{ pkg.category.category }}
    +
    +
    +

    Package Repositories

    + + {% for repo in repos %} + + + + + {% endfor %} + +
    +
    +
    +

    Releases

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    0.8_________pending
    0.7.2Gimmick2006-05-23
    0.7.1Noodle2006-01-05
    0.7Wombat2005-01-24
    0.6Widget2004-03-01
    0.5Nova2003-07-21
    0.4Dragon2002-12-18
    0.3Firefly2002-08-07
    0.2Vega2002-04-17
    0.1Homer2002-03-11
    +
    +
    +

    Documentation:

    + +

    Support Arch:

    + +

    Community Links:

    + +

    Development:

    + +{% endblock %} diff --git a/templates/public/irc.html b/templates/public/irc.html new file mode 100644 index 0000000..cbd00da --- /dev/null +++ b/templates/public/irc.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    IRC Channels

    +

    +

    You can find Arch-related discussion on the following IRC channels. + All channels are on irc.freenode.net

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #archlinuxThe main discussion channel, mostly in English
    #archlinux-bugsBug-centric discussion
    #archlinuxfrDiscussion (French)
    #archlinux.deDiscussion (German)
    #archlinux.seDiscussion (Swedish)
    #archlinux.dkDiscussion (Danish)
    #archlinux-esDiscussion (Spanish)
    #archlinux.brDiscussion (Brazilian Community)
    +
    +

    +{% endblock %} + diff --git a/templates/public/moreforums.html b/templates/public/moreforums.html new file mode 100644 index 0000000..f8a1362 --- /dev/null +++ b/templates/public/moreforums.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    More Arch Forums

    +

    +

    + Our main forum is located at bbs.archlinux.org. + However, there are other locale-specific forums available. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Brazilianhttp://forum.archlinux-br.org
    Czechhttp://forum.archlinux.cz/
    Danishhttp://forum.archlinux.dk/
    Dutchhttp://arch-forum.nl/
    Frenchhttp://forums.archlinuxfr.org
    Frenchhttp://forums.archlinux.fr
    Germanhttp://forum.archlinux.de
    Hungarianhttp://archlinux.hu/forum/
    Italianhttp://www.archlinux.it/forum/
    Polishhttp://forum.arch-linux.pl
    Russianhttp://archlinux.org.ru/forum/
    Spanishhttp://www.archlinux.com.ar/foros/
    Swedenhttp://forum.archlinux.se/ +
    Turkishhttp://forum.linux-sevenler.org/index.php/board,65.0.html
    Ukrainianhttp://archlinux.org.ua/
    +
    + If you have a forum you would like linked, please open a Bug Ticket with the category "web site", and a relevant description. +
    +

    +{% endblock %} + diff --git a/templates/public/press.html b/templates/public/press.html new file mode 100644 index 0000000..b652c8d --- /dev/null +++ b/templates/public/press.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Who's Talking About Arch?

    +

    + Lots of people. + +
    + If you have some press you would like linked, please open a Bug Ticket with the category "web site", and a relevant description. +
    +

    +{% endblock %} + diff --git a/templates/public/projects.html b/templates/public/projects.html new file mode 100644 index 0000000..c669d04 --- /dev/null +++ b/templates/public/projects.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Arch Related Projects

    +

    +

    There are a few Arch-based projects or communities that have sprung up + over the years. Here's a list of the ones we know about.

    + + + + + + + + + + + + + + + + + + + + + +
    user-contributions.orgA website belongs to members wanting to give a little something to the free software community
    ArchStatsAn opt-in system that tracks which packages each user has installed, hardware specs, etc
    Archie Live CDA live CD (and live CD build scripts) based on Arch
    ArchPPCArch packages optimized for PPC
    Hardware DetectionHardware detection scripts for Arch (deprecated in favor of udev's auto-detection)
    AEGISArch Environmental/Geographical Information Systems (AEGIS) Project
    +
    + If you have an Arch related project you would like linked, please open a Bug Ticket with the category "web site", and a relevant description. +
    +

    +{% endblock %} + diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..b3b264c --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} + +
    +

    Developer Login

    +{% if form.has_errors %} +

    Your username and password didn't match. Please try again.

    +{% endif %} +

    + +
    + + + + + +
    {{ form.username }}
    {{ form.password }}
    +
    +
    + +{% endblock %} + diff --git a/templates/registration/logout.html b/templates/registration/logout.html new file mode 100644 index 0000000..2b82986 --- /dev/null +++ b/templates/registration/logout.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} + +
    +You've been logged out. +
    + +{% endblock %} + diff --git a/templates/status_page.html b/templates/status_page.html new file mode 100644 index 0000000..558c654 --- /dev/null +++ b/templates/status_page.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    {{ message }}

    +
    +{% endblock %} + diff --git a/templates/todolists/add.html b/templates/todolists/add.html new file mode 100644 index 0000000..a53637c --- /dev/null +++ b/templates/todolists/add.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Add ToDo List

    +
    + + + + + + + + + + + + + +
    Name:
    Description:
    List of Package Names:
    (one per line)
    + +
    +
    +
    +{% endblock %} diff --git a/templates/todolists/list.html b/templates/todolists/list.html new file mode 100644 index 0000000..0e659f9 --- /dev/null +++ b/templates/todolists/list.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} +
    + {% if perms.todolists.add_todolist %} + + {% endif %} +

    Package ToDo lists

    + + + + + + + + + {% for list in lists %} + + + + + + + + {% endfor %} +
    NameCreation DateCreatorDescriptionStatus
    {{ list.name }}{{ list.date_added }}{{ list.creator.get_full_name }}{{ list.description }}{% if list.complete %}Complete{% else %}Incomplete{% endif %}
    +
    +{% endblock %} diff --git a/templates/todolists/view.html b/templates/todolists/view.html new file mode 100644 index 0000000..89a07a9 --- /dev/null +++ b/templates/todolists/view.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    ToDo List: {{ list.name }}

    + + + + + + + + + {% for pkg in pkgs %} + + + + + + + + {% endfor %} +
    IDRepoNameMaintainerStatus
    {{ pkg.pkg.id }}{{ pkg.pkg.repo.name }}{{ pkg.pkg.pkgname }}{{ pkg.pkg.maintainer.get_full_name }} + {% if pkg.complete %} + Complete + {% else %} + Incomplete + {% endif %} +
    +
    +{% endblock %} diff --git a/templates/wiki/base.html b/templates/wiki/base.html new file mode 100644 index 0000000..90d9315 --- /dev/null +++ b/templates/wiki/base.html @@ -0,0 +1,30 @@ + + + +{% block title %}{% endblock %} + + +{% block content %} +

    This is the base template. Extend it with the "extends" template tag.

    +{% endblock %} + + \ No newline at end of file diff --git a/templates/wiki/edit.html b/templates/wiki/edit.html new file mode 100644 index 0000000..421a313 --- /dev/null +++ b/templates/wiki/edit.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block content %} +
    + +

    Editing: {{ page.title }}

    +
    + +

    + +

    + +
    + +
    +{% endblock %} diff --git a/templates/wiki/home.html b/templates/wiki/home.html new file mode 100644 index 0000000..6c0fb6e --- /dev/null +++ b/templates/wiki/home.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +
    +

    Wiki Index

    +{% for page in pages %} +

    {{ page.title }}

    +{% endfor %} +
    +{% endblock %} diff --git a/templates/wiki/page.html b/templates/wiki/page.html new file mode 100644 index 0000000..e0e6ddd --- /dev/null +++ b/templates/wiki/page.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% load wikitags %} +{% block content %} +
    +

    {{ page.title }}

    +
    +{{ page.content|wikify }} +
    + + +
    + Last Author: {{ page.last_author.username }} +
    +
    +{% endblock %} diff --git a/todolists/__init__.py b/todolists/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/todolists/models.py b/todolists/models.py new file mode 100644 index 0000000..0a4f445 --- /dev/null +++ b/todolists/models.py @@ -0,0 +1,31 @@ +from django.db import models +from django.contrib.auth.models import User +from archlinux.packages.models import Package + +class TodolistManager(models.Manager): + def get_incomplete(self): + results = [] + for l in self.all().order_by('-date_added'): + if TodolistPkg.objects.filter(list=l.id).filter(complete=False).count() > 0: + results.append(l) + return results + +class Todolist(models.Model): + id = models.AutoField(primary_key=True) + creator = models.ForeignKey(User) + name = models.CharField(maxlength=255) + description = models.TextField() + date_added = models.DateField(auto_now_add=True) + objects = TodolistManager() + class Meta: + db_table = 'todolists' + +class TodolistPkg(models.Model): + id = models.AutoField(primary_key=True) + list = models.ForeignKey(Todolist) + pkg = models.ForeignKey(Package) + complete = models.BooleanField(default=False) + class Meta: + db_table = 'todolists_pkgs' + unique_together = (('list','pkg'),) + diff --git a/todolists/views.py b/todolists/views.py new file mode 100644 index 0000000..e364f36 --- /dev/null +++ b/todolists/views.py @@ -0,0 +1,64 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.models import User +from archlinux.utils import render_template +from archlinux.todolists.models import Todolist, TodolistPkg +from archlinux.packages.models import Package + +# FIXME: ugly hackery. http://code.djangoproject.com/ticket/3450 +import django.db +IntegrityError = django.db.backend.Database.IntegrityError + +@login_required +#@is_maintainer +def flag(request, listid, pkgid): + list = get_object_or_404(Todolist, id=listid) + pkg = get_object_or_404(TodolistPkg, id=pkgid) + pkg.complete = not pkg.complete + pkg.save() + return HttpResponseRedirect('/todo/%s/' % (listid)) + +@login_required +def view(request, listid): + list = get_object_or_404(Todolist, id=listid) + pkgs = TodolistPkg.objects.filter(list=list.id).order_by('pkg') + return render_template('todolists/view.html', request, {'list':list,'pkgs':pkgs}) + +@login_required +def list(request): + lists = Todolist.objects.order_by('-date_added') + for l in lists: + l.complete = TodolistPkg.objects.filter(list=l.id,complete=False).count() == 0 + return render_template('todolists/list.html', request, {'lists':lists}) + +@login_required +#@is_maintainer +@user_passes_test(lambda u: u.has_perm('todolists.add_todolist')) +def add(request): + if request.POST: + try: + m = User.objects.get(username=request.user.username) + except User.DoesNotExist: + return render_template('error_page.html', request, + {'errmsg': 'Cannot find a maintainer record for you!'}) + # create the list + todo = Todolist( + creator = m, + name = request.POST.get('name'), + description = request.POST.get('description')) + todo.save() + # now link in packages + for p in request.POST.get('packages').split("\n"): + for pkg in Package.objects.filter(pkgname=p.strip()): + todopkg = TodolistPkg( + list = todo, + pkg = pkg) + try: + todopkg.save() + except IntegrityError, (num, desc): + if num == 1062: # duplicate entry aka dupe package on list + pass + return HttpResponseRedirect('/todo/') + + return render_template('todolists/add.html', request) diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..bf6f18d --- /dev/null +++ b/urls.py @@ -0,0 +1,71 @@ +from django.conf.urls.defaults import * +from archlinux.news.models import News +from archlinux.feeds import PackageFeed, NewsFeed + +feeds = { + 'packages': PackageFeed, + 'news': NewsFeed +} + +urlpatterns = patterns('', + (r'^media/(.*)$', 'django.views.static.serve', {'document_root': '/home/jvinet/shared/work/archlinux/media'}), + +# Dynamic Stuff + (r'^packages/flag/(\d+)/$', 'archlinux.packages.views.flag'), + (r'^packages/flaghelp/$', 'archlinux.packages.views.flaghelp'), + (r'^packages/unflag/(\d+)/$', 'archlinux.packages.views.unflag'), + (r'^packages/files/(\d+)/$', 'archlinux.packages.views.files'), + (r'^packages/search/$', 'archlinux.packages.views.search'), + (r'^packages/search/([A-z0-9]+)/$', 'archlinux.packages.views.search'), + (r'^packages/update/$', 'archlinux.packages.views.update'), + (r'^packages/(?P\d+)/$', 'archlinux.packages.views.details'), + (r'^packages/(?P[A-z0-9]+)/$', 'archlinux.packages.views.details'), + (r'^packages/(?P[A-z0-9]+)/(?P[A-z0-9]+)/$', 'archlinux.packages.views.details'), + (r'^packages/$', 'archlinux.packages.views.search'), + + (r'^todo/(\d+)/$', 'archlinux.todolists.views.view'), + (r'^todo/add/$', 'archlinux.todolists.views.add'), + (r'^todo/flag/(\d+)/(\d+)/$', 'archlinux.todolists.views.flag'), + (r'^todo/$', 'archlinux.todolists.views.list'), + + (r'^news/(\d+)/$', 'archlinux.news.views.view'), + (r'^news/add/$', 'archlinux.news.views.add'), + (r'^news/edit/(\d+)/$', 'archlinux.news.views.edit'), + (r'^news/delete/(\d+)/$', 'archlinux.news.views.delete'), + (r'^news/$', 'archlinux.news.views.list'), + + (r'^devel/$', 'archlinux.devel.views.index'), + (r'^devel/notify/$', 'archlinux.devel.views.change_notify'), + (r'^devel/profile/$', 'archlinux.devel.views.change_profile'), + (r'^devel/guide/$', 'archlinux.devel.views.guide'), + + (r'^wiki/([A-Z]+[A-z0-9 :/-]+)/$', 'archlinux.wiki.views.page'), + (r'^wiki/edit/([A-Z]+[A-z0-9 :/-]+)/$', 'archlinux.wiki.views.edit'), + (r'^wiki/delete/$', 'archlinux.wiki.views.delete'), + (r'^wiki/index/$', 'archlinux.wiki.views.index'), + (r'^wiki/$', 'archlinux.wiki.views.main'), + +# Feeds + (r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}), + +# (mostly) Static Pages + (r'^$', 'archlinux.public.views.index'), + (r'^about/$', 'archlinux.public.views.about'), + (r'^art/$', 'archlinux.public.views.art'), + (r'^cvs/$', 'archlinux.public.views.cvs'), + (r'^developers/$', 'archlinux.public.views.developers'), + (r'^donate/$', 'archlinux.public.views.donate'), + (r'^download/$', 'archlinux.public.views.download'), + (r'^irc/$', 'archlinux.public.views.irc'), + (r'^moreforums/$', 'archlinux.public.views.moreforums'), + (r'^press/$', 'archlinux.public.views.press'), + (r'^projects/$', 'archlinux.public.views.projects'), + +# Authentication / Admin + (r'^denied/$', 'archlinux.public.views.denied'), + (r'^login/$', 'django.contrib.auth.views.login', {'template_name': 'registration/login.html'}), + (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'registration/login.html'}), + (r'^logout/$', 'django.contrib.auth.views.logout', {'template_name': 'registration/logout.html'}), + (r'^accounts/logout/$', 'django.contrib.auth.views.logout', {'template_name': 'registration/logout.html'}), + (r'^admin/', include('django.contrib.admin.urls')), +) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..7848fce --- /dev/null +++ b/utils.py @@ -0,0 +1,69 @@ +from django.core import validators +from django.template import RequestContext +from django.shortcuts import render_to_response +from django.http import HttpResponseRedirect +from string import * +import sgmllib + +#from archlinux.packages.models import Maintainer +#from archlinux.settings import BADPRIVS_URL +#def is_maintainer(view_func, badprivs_url=BADPRIVS_URL): +# """ +# Decorator for views that checks that the logged-in user has a corresponding +# record in the Maintainers table. If not, the user is forwarded to a +# "bad-privileges" page. +# """ +# def _dec(view_func): +# def _checkuser(request, *args, **kwargs): +# try: +# m = Maintainer.objects.get(username=request.user.username) +# except Maintainer.DoesNotExist: +# return HttpResponseRedirect(badprivs_url) +# return view_func(request, *args, **kwargs) +# +# return _checkuser +# return _dec(view_func) + +def render_template(template, request, context=None): + """ + A shortcut to render_to_response with a RequestContext. Also includes + request.path in the context, so both 'path' and 'user' are accessible to + the template. + """ + if context: + context['path'] = request.path + return render_to_response(template, context_instance=RequestContext(request, context)) + else: + return render_to_response(template, context_instance=RequestContext(request)) + +def validate(errdict, fieldname, fieldval, validator, blankallowed, request): + """ + A helper function that allows easy access to Django's validators without + going through a Manipulator object. Will return a dict of all triggered + errors. + """ + if blankallowed and not fieldval: + return + alldata = ' '.join(request.POST.values()) + ' '.join(request.GET.values()) + try: + validator(fieldval, alldata) + except validators.ValidationError, e: + if not errdict.has_key(fieldname): errdict[fieldname] = [] + errdict[fieldname].append(e) + + +# XXX: unused right now, probably not needed +class Stripper(sgmllib.SGMLParser): + """Helper class to strip HTML tags""" + def __init__(self): + sgmllib.SGMLParser.__init__(self) + + def strip(self, some_html): + """Strips all HTML tags and leading/trailing whitespace""" + self.theString = "" + self.feed(some_html) + self.close() + return self.theString + + def handle_data(self, data): + self.theString += data diff --git a/wiki/__init__.py b/wiki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wiki/models.py b/wiki/models.py new file mode 100644 index 0000000..2b8b16f --- /dev/null +++ b/wiki/models.py @@ -0,0 +1,16 @@ +from django.db import models +from django.contrib.auth.models import User + +class Wikipage(models.Model): + """Wiki page storage""" + title = models.CharField(maxlength=255) + content = models.TextField() + last_author = models.ForeignKey(User) + class Meta: + db_table = 'wikipages' + + def editurl(self): + return "/wiki/edit/" + self.title + "/" + + def __repr__(self): + return self.title diff --git a/wiki/templatetags/__init__.py b/wiki/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wiki/templatetags/wikitags.py b/wiki/templatetags/wikitags.py new file mode 100644 index 0000000..bdf8df6 --- /dev/null +++ b/wiki/templatetags/wikitags.py @@ -0,0 +1,57 @@ +from django.template import Library +from django.conf import settings +from archlinux.lib import markdown +import re + +register = Library() + +class WikiProcessor: + def run(self, lines): + in_table = False + for i in range(len(lines)): + # Linebreaks + lines[i] = re.sub("%%", "
    ", lines[i]) + # Internal Links + lines[i] = re.sub("\(\(([A-z0-9 :/-]+)\)\)", "\\1", lines[i]) + # Small Text + lines[i] = re.sub("----([^----]+)----", "\\1", lines[i]) + lines[i] = re.sub("--([^--]+)--", "\\1", lines[i]) + # TT text + lines[i] = re.sub("\{\{([^}\}]+)\}\}", "\\1", lines[i]) + # Tables + m = re.match("(\|\|)", lines[i]) + if m: + count = len(re.findall("(\|\|+)", lines[i])) + first = True + m2 = re.search("(\|\|+)", lines[i]) + while m2 and count: + count -= 1 + colspan = len(m2.group(1)) / 2 + if first: + repl = "" % (colspan) + first = False + elif count == 0: + repl = "" + else: + repl = "" % (colspan) + lines[i] = re.sub("(\|\|+)", repl, lines[i], 1) + # find the next chunk + m2 = re.search("(\|\|+)", lines[i]) + lines[i] = "" + lines[i] + "" + if not in_table: + lines[i] = "" + lines[i] + in_table = True + elif in_table: + lines[i] = "
    " + lines[i] + in_table = False + # close leftover table, if open + if in_table: + lines[len(lines)] = lines[len(lines)] + "" + return lines + +@register.filter +def wikify(value): + md = markdown.Markdown(value) + md.preprocessors.insert(0, WikiProcessor()) + html = md.toString() + return html diff --git a/wiki/views.py b/wiki/views.py new file mode 100644 index 0000000..c78da37 --- /dev/null +++ b/wiki/views.py @@ -0,0 +1,61 @@ +# +# Based on code from http://e-scribe.com/news/210 +# +from django.http import HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from archlinux.utils import render_template +from archlinux.wiki.models import Wikipage + +@login_required +def index(request): + """Return a list of all wiki pages""" + pages = Wikipage.objects.all().order_by('title') + return render_template('wiki/home.html', request, {'pages':pages}) + +def main(request): + """Return the Index wiki page""" + return HttpResponseRedirect("/wiki/WikiIndex/") + +@login_required +def page(request, title): + """Display page, or redirect to root if page doesn't exist yet""" + try: + page = Wikipage.objects.get(title__exact=title) + return render_template('wiki/page.html', request, {'page':page}) + except Wikipage.DoesNotExist: + return HttpResponseRedirect("/wiki/edit/%s/" % title) + +@login_required +def edit(request, title): + """Process submitted page edits (POST) or display editing form (GET)""" + if request.POST: + try: + page = Wikipage.objects.get(title__exact=title) + except Wikipage.DoesNotExist: + # Must be a new one; let's create it + page = Wikipage(title=title) + page.content = request.POST['content'] + page.title = request.POST['title'] + page.last_author = request.user + page.save() + return HttpResponseRedirect("/wiki/" + page.title + "/") + else: + try: + page = Wikipage.objects.get(title__exact=title) + except Wikipage.DoesNotExist: + # create a dummy page object -- note that it is not saved! + page = Wikipage(title=title) + page.body = "" + return render_template('wiki/edit.html', request, {'page':page}) + +@login_required +def delete(request): + """Delete a page""" + if request.POST: + title = request.POST['title'] + try: + page = Wikipage.objects.get(title__exact=title) + except Wikipage.DoesNotExist: + return HttpResponseRedirect("/wiki/") + page.delete() + return HttpResponseRedirect("/wiki/") -- cgit v1.2.3-24-g4f1b
  7. ' + dep + '