From 5da3a0f5d37070cc1c91b455842a80e2b44f274f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 27 Sep 2008 23:50:03 +0900 Subject: [PATCH] initial commit from hg --- .gitignore | 9 + COPYING | 676 +++ ChangeLog.old | 5091 +++++++++++++++++ anki/__init__.py | 58 + anki/cards.py | 250 + anki/db.py | 100 + anki/deck.py | 1825 ++++++ anki/errors.py | 58 + anki/exporting.py | 218 + anki/facts.py | 150 + anki/features/__init__.py | 65 + anki/features/chinese/README | 4 + anki/features/chinese/__init__.py | 89 + anki/features/chinese/save_unihan.py | 101 + anki/features/japanese.py | 106 + anki/fonts.py | 55 + anki/graphs.py | 216 + anki/history.py | 70 + anki/importing/__init__.py | 231 + anki/importing/anki03.py | 285 + anki/importing/anki10.py | 58 + anki/importing/csv.py | 129 + anki/importing/mnemosyne10.py | 76 + anki/importing/wcu.py | 57 + anki/lang.py | 59 + anki/latex.py | 113 + anki/locale/libanki_cs_CZ.po | 744 +++ anki/locale/libanki_de_DE.po | 678 +++ anki/locale/libanki_es_ES.po | 815 +++ anki/locale/libanki_fr_FR.po | 813 +++ anki/locale/libanki_ja_JP.po | 638 +++ anki/locale/libanki_ko_KR.po | 677 +++ anki/locale/messages.pot | 638 +++ anki/media.py | 201 + anki/models.py | 255 + anki/sound.py | 95 + anki/stats.py | 581 ++ anki/stdmodels.py | 210 + anki/sync.py | 821 +++ anki/utils.py | 173 + ez_setup.py | 228 + setup.cfg | 0 setup.py | 34 + tests/__init__.py | 0 tests/importing/test.mem | 219 + tests/importing/test03.anki | Bin 0 -> 3933 bytes tests/importing/test10-2.anki | Bin 0 -> 31744 bytes tests/importing/test10.anki | Bin 0 -> 31744 bytes tests/importing/text-2fields.txt | 9 + tests/shared.py | 7 + tests/syncing/media-tests/1.anki | Bin 0 -> 43008 bytes .../834a227f8d0abc4e2193f08138e59885.png | Bin 0 -> 545 bytes .../c4ad64a7afe9b09602cdf91e18897959.png | Bin 0 -> 580 bytes tests/syncing/media-tests/2.anki | Bin 0 -> 43008 bytes .../22161b29b0c18e068038021f54eee1ee.png | Bin 0 -> 644 bytes tests/test_deck.py | 139 + tests/test_exporting.py | 80 + tests/test_features.py | 31 + tests/test_importing.py | 64 + tests/test_stdmodels.py | 22 + tests/test_sync.py | 276 + tests/test_utils.py | 18 + tools/tests.sh | 8 + tools/translate.sh | 30 + 64 files changed, 18653 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 ChangeLog.old create mode 100644 anki/__init__.py create mode 100644 anki/cards.py create mode 100644 anki/db.py create mode 100644 anki/deck.py create mode 100644 anki/errors.py create mode 100644 anki/exporting.py create mode 100644 anki/facts.py create mode 100644 anki/features/__init__.py create mode 100644 anki/features/chinese/README create mode 100644 anki/features/chinese/__init__.py create mode 100644 anki/features/chinese/save_unihan.py create mode 100644 anki/features/japanese.py create mode 100644 anki/fonts.py create mode 100644 anki/graphs.py create mode 100644 anki/history.py create mode 100644 anki/importing/__init__.py create mode 100644 anki/importing/anki03.py create mode 100644 anki/importing/anki10.py create mode 100644 anki/importing/csv.py create mode 100644 anki/importing/mnemosyne10.py create mode 100644 anki/importing/wcu.py create mode 100644 anki/lang.py create mode 100644 anki/latex.py create mode 100644 anki/locale/libanki_cs_CZ.po create mode 100644 anki/locale/libanki_de_DE.po create mode 100644 anki/locale/libanki_es_ES.po create mode 100644 anki/locale/libanki_fr_FR.po create mode 100644 anki/locale/libanki_ja_JP.po create mode 100644 anki/locale/libanki_ko_KR.po create mode 100644 anki/locale/messages.pot create mode 100644 anki/media.py create mode 100644 anki/models.py create mode 100644 anki/sound.py create mode 100644 anki/stats.py create mode 100644 anki/stdmodels.py create mode 100644 anki/sync.py create mode 100644 anki/utils.py create mode 100644 ez_setup.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/importing/test.mem create mode 100644 tests/importing/test03.anki create mode 100644 tests/importing/test10-2.anki create mode 100644 tests/importing/test10.anki create mode 100644 tests/importing/text-2fields.txt create mode 100644 tests/shared.py create mode 100644 tests/syncing/media-tests/1.anki create mode 100644 tests/syncing/media-tests/1.media/834a227f8d0abc4e2193f08138e59885.png create mode 100644 tests/syncing/media-tests/1.media/c4ad64a7afe9b09602cdf91e18897959.png create mode 100644 tests/syncing/media-tests/2.anki create mode 100644 tests/syncing/media-tests/2.media/22161b29b0c18e068038021f54eee1ee.png create mode 100644 tests/test_deck.py create mode 100644 tests/test_exporting.py create mode 100644 tests/test_features.py create mode 100644 tests/test_importing.py create mode 100644 tests/test_stdmodels.py create mode 100644 tests/test_sync.py create mode 100644 tests/test_utils.py create mode 100755 tools/tests.sh create mode 100755 tools/translate.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8563b9750 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.pyc +*~ +*.mo +*\# +build +dist +anki.egg-info +samples +unihan.db diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..443254047 --- /dev/null +++ b/COPYING @@ -0,0 +1,676 @@ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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. But first, please read +. + diff --git a/ChangeLog.old b/ChangeLog.old new file mode 100644 index 000000000..291acbfb0 --- /dev/null +++ b/ChangeLog.old @@ -0,0 +1,5091 @@ +changeset: 794:7f344e1ac094 +tag: tip +user: Damien Elmes +date: Sat Sep 27 02:20:35 2008 +0900 +description: +rebuild priorities on server too after sync + +changeset: 793:8d66489049d1 +user: Damien Elmes +date: Wed Sep 24 19:51:03 2008 +0900 +description: +simplify algo in media, support foreign chars in sync + +changeset: 792:747601f9084f +user: Damien Elmes +date: Tue Sep 23 03:21:35 2008 +0900 +description: +update german translations + +changeset: 791:6455da654ada +user: Damien Elmes +date: Tue Sep 23 03:12:57 2008 +0900 +description: +rollback earlier failed card behaviour - needs more thought + +changeset: 790:3f3b36a19b14 +user: Damien Elmes +date: Tue Sep 23 02:55:06 2008 +0900 +description: +update delay handling of older cards + +changeset: 789:dc401f352309 +user: Damien Elmes +date: Tue Sep 23 02:29:52 2008 +0900 +description: +don't touch latex cache files when clearing media dir + +changeset: 788:d41a997c82b3 +user: Damien Elmes +date: Tue Sep 23 02:25:38 2008 +0900 +description: +improve media tagging, add untagging + +changeset: 787:b8bdcfdfc62e +user: Damien Elmes +date: Tue Sep 23 02:13:05 2008 +0900 +description: +fix typo in stats + +changeset: 786:330eeaffcedc +user: Damien Elmes +date: Mon Sep 22 19:19:19 2008 +0900 +description: +bump version number + +changeset: 785:7aa37d460ffa +user: Damien Elmes +date: Mon Sep 22 19:17:57 2008 +0900 +description: +on failure of card in learning state, don't allow 7-9 & ignore delay + +changeset: 784:1a0ae8c0fd75 +user: Damien Elmes +date: Mon Sep 15 16:20:04 2008 +0900 +description: +include timestamp in getDecks() sync msg + +changeset: 783:e4b103adfcee +user: Damien Elmes +date: Mon Sep 22 17:07:21 2008 +0900 +description: +update translations, make some extra fields translatable + +changeset: 782:e1f8f469a2d4 +user: Damien Elmes +date: Mon Sep 22 15:58:27 2008 +0900 +description: +update translations, add media tests + +changeset: 781:bea16d75308c +user: Damien Elmes +date: Mon Sep 22 15:35:44 2008 +0900 +description: +add default priority tags + +changeset: 780:1b6195fa184f +user: Damien Elmes +date: Mon Sep 22 04:02:47 2008 +0900 +description: +avoid bumping card mod times, as upgrade happens both sides + +changeset: 779:cd0b3d65ebae +user: Damien Elmes +date: Mon Sep 22 03:37:37 2008 +0900 +description: +restore sync url + +changeset: 778:9558de1cbce3 +user: Damien Elmes +date: Mon Sep 22 03:36:15 2008 +0900 +description: +updates to media syncing, db handling, make sync control easier + +changeset: 777:f25fb49ea76a +user: Damien Elmes +date: Sun Sep 21 19:32:11 2008 +0900 +description: +refactor media code, bugfixes + +changeset: 776:1044f2d83a37 +user: Damien Elmes +date: Sat Sep 20 03:55:48 2008 +0900 +description: +speed up 'add missing cards' + +changeset: 775:605f97bea2e9 +user: Damien Elmes +date: Sat Sep 20 03:50:01 2008 +0900 +description: +implement media syncing, making syncing easier, refactor string ids, more +- support optional syncing of media +- generate string ids with ids2str +- use random ids for media (this will error if user adds same file on both +sides w/o syncing) +- avoid rebuilding media dir twice for those who aren't running the latest +version +- fix bugs with media rebuild code +- add prepareSync() to handle half the syncing which doesn't change + +changeset: 774:de980b3d67c9 +user: Damien Elmes +date: Fri Sep 19 14:03:17 2008 +0900 +description: +media: keep use count, ignore dirs, track deletions by filename + +changeset: 773:dec7f440bda1 +user: Damien Elmes +date: Wed Sep 17 20:22:19 2008 +0900 +description: +fix problem upgrading decks on case sensitive filesystems + +changeset: 772:de1503bf9a60 +user: Damien Elmes +date: Wed Sep 17 17:45:59 2008 +0900 +description: +new stats + +changeset: 771:eb086fba0d33 +user: Damien Elmes +date: Wed Sep 17 00:48:54 2008 +0900 +description: +fixes to media upgrading +- use random dir name for backups +- catch more than one media reference per field +- avoid two checksums +- support same media in multiple fields +- ignore dot files + +changeset: 770:d11f66084bbc +user: Damien Elmes +date: Tue Sep 16 12:51:09 2008 +0900 +description: +cards default to new, rebuild types on checkdb, fix importing new + +changeset: 769:a69f2aeae74b +user: Damien Elmes +date: Mon Sep 15 05:11:47 2008 +0900 +description: +bump version number + +changeset: 768:90440fcb1702 +user: Damien Elmes +date: Mon Sep 15 05:11:23 2008 +0900 +description: +catch img uses, not just audio uses + +changeset: 767:bf971ef03af9 +user: Damien Elmes +date: Mon Sep 15 02:15:13 2008 +0900 +description: +fix for previous dailyStats fix + +changeset: 766:607ed6876ee4 +user: Damien Elmes +date: Mon Sep 15 02:06:25 2008 +0900 +description: +only build 'newCountLeftToday' if building queue + +changeset: 765:0bc83189a028 +user: Damien Elmes +date: Mon Sep 15 01:16:38 2008 +0900 +description: +fix div by zero error + +changeset: 764:30f16e91c869 +user: Damien Elmes +date: Mon Sep 15 01:08:12 2008 +0900 +description: +enable upgrade + +changeset: 763:77bdb561bcdb +user: Damien Elmes +date: Mon Sep 15 01:00:13 2008 +0900 +description: +bump version + +changeset: 762:7068d1cb2ad5 +user: Damien Elmes +date: Mon Sep 15 00:48:56 2008 +0900 +description: +revert importing changes + +changeset: 761:f7ad4f6130dd +user: Damien Elmes +date: Mon Sep 15 00:36:25 2008 +0900 +description: +support adding tags to cards, update chinese tests + +changeset: 760:71fbab934ae2 +user: Damien Elmes +date: Mon Sep 15 00:25:31 2008 +0900 +description: +new unihan db + +changeset: 759:c5ccbd1927f6 +user: Damien Elmes +date: Mon Sep 15 00:25:16 2008 +0900 +description: +fix save_unihan to support multiple readings + +changeset: 758:b8f797202f95 +user: Damien Elmes +date: Sun Sep 14 23:19:29 2008 +0900 +description: +check average factor on startup, set new cards to average factor + +changeset: 757:5b5c9b7830fe +user: Damien Elmes +date: Sun Sep 14 22:21:05 2008 +0900 +description: +update translations + +changeset: 756:be99b4ba4a28 +user: Damien Elmes +date: Sun Sep 14 22:19:58 2008 +0900 +description: +improve timer + +changeset: 755:cdcee481960e +user: Damien Elmes +date: Sun Sep 14 20:28:27 2008 +0900 +description: +fix bug in unihan gen, use real pinyin readings thanks to patch by daniel chai + +changeset: 754:3f5f57d21fe0 +user: Damien Elmes +date: Sun Sep 14 19:48:54 2008 +0900 +description: +don't stop timer when calling thinkingTime() + +changeset: 753:40b99029d15f +user: Damien Elmes +date: Sun Sep 14 16:14:19 2008 +0900 +description: +improve media upgrade, backup all files + +changeset: 752:5204c80a9a50 +user: Damien Elmes +date: Sun Sep 14 15:19:20 2008 +0900 +description: +new media support, scheduling changes +- media support moved into separate module +- checksum files on add, and rename them to checksummed version +- tidy up new card scheduling routines +- define newCountLeftToday, and use it in scheduling +- limit new card count to max new cards per day +- when using 'distribute new cards', calculate eta based on new+old, not one +or the other +- remove distractedTime/reps support - it's not useful +- add routine to check media dir for dangling links +- store media descriptions in new table + +changeset: 751:a39c3a8fa613 +user: Damien Elmes +date: Wed Sep 10 13:32:22 2008 +0900 +description: +fix statement call in fixIntegrity() + +changeset: 750:ea41a60e9bfb +user: Damien Elmes +date: Wed Sep 10 00:05:45 2008 +0900 +description: +change order of version bump on version 0 upgrade + +changeset: 749:c989e349c6c7 +user: Damien Elmes +date: Wed Sep 10 00:03:12 2008 +0900 +description: +add views and indices on check integrity. should fix some upgrade bugs + +changeset: 748:5ef19bdc5dc1 +user: Damien Elmes +date: Mon Sep 08 23:43:34 2008 +0900 +description: +only rebuild types on upgrade, optimise syncing +- since low & very high priority cards aren't put in a different queue +anymore, there's no need to rebuild their types on sync. this saves about 5 +seconds on the iphone on a deck of 4000 cards +- furthermore, we can avoid rebuilding the priorities of cards that weren't +transferred in the sync. this saves another 5 seconds +- and we can take the minimum of lastSync rather than setting to zero, to +avoid sending the whole summary. need to check to make sure this won't cause +problems + +changeset: 747:a52a2a8d5102 +user: Damien Elmes +date: Sun Sep 07 00:05:58 2008 +0900 +description: +update spanish translations + +changeset: 746:3d24c220cf3c +user: Damien Elmes +date: Sat Sep 06 23:50:26 2008 +0900 +description: +bump version number + +changeset: 745:1b3ddc8a0d57 +user: Damien Elmes +date: Sat Sep 06 22:29:09 2008 +0900 +description: +put distribute cards first + +changeset: 744:68e39d789164 +user: Damien Elmes +date: Sat Sep 06 22:26:38 2008 +0900 +description: +remove 'new cards first' option + +changeset: 743:b2124055af6c +user: Damien Elmes +date: Sat Sep 06 21:54:26 2008 +0900 +description: +fix bug in getCardIds + +changeset: 742:8b2a0ad58993 +user: Damien Elmes +date: Sat Sep 06 21:42:38 2008 +0900 +description: +don't force a check - it'll cause problems with syncing + +changeset: 741:9d0dd086b846 +user: Damien Elmes +date: Sat Sep 06 21:29:24 2008 +0900 +description: +new card handling, multiple schedulers, integrity improvs, more +- three different scheduling choices - default spreads new cards out +throughout reviews +- limit number of new cards per day to 20 by default +- refactor getCardIds() so that new card handling is implementable. slightly +slower, but only an issue for the website, not desktop clients +- don't change queue for very high priority and low priority +- show number of new cards due next day on finish + +changeset: 740:dee624693448 +user: Damien Elmes +date: Wed Sep 03 15:22:59 2008 +0900 +description: +translate days/cards in graphs + +changeset: 739:55ee54b34790 +user: Damien Elmes +date: Wed Sep 03 04:35:18 2008 +0900 +description: +bump version number + +changeset: 738:a07944b55ff5 +user: Damien Elmes +date: Wed Sep 03 04:04:32 2008 +0900 +description: +checkdb: regenerate question/answer cache, and mark all cards/facts/models mod + +changeset: 737:c6fbfb44d925 +user: Damien Elmes +date: Wed Sep 03 03:47:15 2008 +0900 +description: +don't forget deletions when checking db + +changeset: 736:fba4ad426675 +user: Damien Elmes +date: Wed Sep 03 03:17:34 2008 +0900 +description: +add german updates from [Bananeweizen at gmx dot de] + +changeset: 735:14b62b62df80 +user: Damien Elmes +date: Wed Sep 03 03:14:00 2008 +0900 +description: +fix some problems with the exporting code +- upgrade fact spacing to use the cards table +- set new card fields like relativeDelay +- set due to creation time + +changeset: 734:00a0a191a2a0 +user: Damien Elmes +date: Wed Sep 03 01:07:10 2008 +0900 +description: +ignore deleted objects on import, fix html & forget deletions on checkdb + +changeset: 733:56fc2a350a49 +user: Damien Elmes +date: Tue Sep 02 16:15:07 2008 +0900 +description: +add tidyHTML to utils, start work on additions to checkDB + +changeset: 732:e577dce1ca01 +user: Damien Elmes +date: Mon Sep 01 20:06:43 2008 +0900 +description: +limit combinedDue to 1 + +changeset: 731:00be0271bfa5 +user: Damien Elmes +date: Mon Sep 01 19:41:41 2008 +0900 +description: +bump version number + +changeset: 730:ab282e5bfee0 +user: Damien Elmes +date: Mon Sep 01 17:34:26 2008 +0900 +description: +fix initial spacing setting high spacing values + +changeset: 729:294cc15863b6 +user: Damien Elmes +date: Sun Aug 31 22:56:06 2008 +0900 +description: +update spanish translations + +changeset: 728:487e131cb90e +user: Damien Elmes +date: Sun Aug 31 22:50:02 2008 +0900 +description: +bump version number + +changeset: 727:309a3b96f57f +user: Damien Elmes +date: Sun Aug 31 22:42:40 2008 +0900 +description: +update graphs code to use new db layout + +changeset: 726:6ae2b8dfa397 +user: Damien Elmes +date: Sun Aug 31 22:37:33 2008 +0900 +description: +set cards to not due when answering - they'll be updated later + +changeset: 725:aa2188746766 +user: Damien Elmes +date: Sun Aug 31 21:27:24 2008 +0900 +description: +update relativeDelay for all cards, not just recently expired ones + +changeset: 724:02fea17b9d22 +user: Damien Elmes +date: Sun Aug 31 17:33:09 2008 +0900 +description: +if card is suspended, set isDue = 0 +- thanks to Nathanael Law for the report + +changeset: 723:5410d31cfadc +user: Damien Elmes +date: Sun Aug 31 17:23:10 2008 +0900 +description: +add support for changing order of card models and field models +- thanks to Nathanael Law for the patch +- updated patch to mark facts/models modified so changes sync + +changeset: 722:a8b75ac64b1e +user: Damien Elmes +date: Sun Aug 31 15:27:03 2008 +0900 +description: +set new interval to 0.001, not 1 + +changeset: 721:fe91fa7e876e +parent: 717:9748f6c99a40 +parent: 720:c3c16d61c5fd +user: Damien Elmes +date: Sun Aug 31 15:15:32 2008 +0900 +description: +merge + +changeset: 720:c3c16d61c5fd +parent: 719:a0178186c744 +parent: 654:c55cd3992387 +user: Damien Elmes +date: Sun Aug 31 15:08:33 2008 +0900 +description: +merge + +changeset: 719:a0178186c744 +parent: 718:a83de27a1d93 +parent: 716:87f9ae70fea1 +user: Damien Elmes +date: Sun Aug 31 15:08:22 2008 +0900 +description: +merge + +changeset: 718:a83de27a1d93 +parent: 714:d10138a2f9f5 +parent: 653:4a530339560c +user: Damien Elmes +date: Sun Aug 31 15:07:53 2008 +0900 +description: +merge + +changeset: 717:9748f6c99a40 +parent: 715:ebb00029c503 +parent: 716:87f9ae70fea1 +user: Damien Elmes +date: Sun Aug 31 15:15:20 2008 +0900 +description: +merge + +changeset: 716:87f9ae70fea1 +parent: 711:c9c5e73e233f +user: Damien Elmes +date: Thu Aug 28 19:39:00 2008 +0900 +description: +always choose oldest model when merging + +changeset: 715:ebb00029c503 +parent: 714:d10138a2f9f5 +parent: 654:c55cd3992387 +user: Damien Elmes +date: Sun Aug 31 15:14:33 2008 +0900 +description: +merge + +changeset: 714:d10138a2f9f5 +user: Damien Elmes +date: Fri Aug 29 21:09:41 2008 +0900 +description: +bump version number + +changeset: 713:78fba5eca19e +user: Damien Elmes +date: Fri Aug 29 20:06:51 2008 +0900 +description: +drop indices only if exist, don't check folder is writeable + +changeset: 712:1d5578fc859e +user: Damien Elmes +date: Fri Aug 29 19:26:41 2008 +0900 +description: +remove obsolote indices, use priority index + +changeset: 711:c9c5e73e233f +user: Damien Elmes +date: Thu Aug 28 17:58:38 2008 +0900 +description: +fix media dir file size check + +changeset: 710:af42a3f9fb5a +user: Damien Elmes +date: Thu Aug 28 17:44:17 2008 +0900 +description: +fix bug setting current model, add model merging support + +changeset: 709:84f9dfcc25fa +user: Damien Elmes +date: Thu Aug 28 16:10:29 2008 +0900 +description: +fix mature/young card counts (ignore priorities) + +changeset: 708:ead7f8877468 +user: Damien Elmes +date: Thu Aug 28 16:07:04 2008 +0900 +description: +fix seenCardCount()/newCardCount() to ignore priorities + +changeset: 707:996de202997e +user: Damien Elmes +date: Wed Aug 27 04:04:11 2008 +0900 +description: +if no earliest time, tell user to add new cards + +changeset: 706:0aa73467b953 +user: Damien Elmes +date: Wed Aug 27 02:49:58 2008 +0900 +description: +catch cards with no card model too + +changeset: 705:7a5f77987b42 +user: Damien Elmes +date: Tue Aug 26 23:18:59 2008 +0900 +description: +make fixIntegrity() syncable, add more checks + +changeset: 704:7cd31348d5d6 +user: Damien Elmes +date: Tue Aug 26 12:42:01 2008 +0900 +description: +update lastInterval/due/factor + +changeset: 703:027099bf01ef +user: Damien Elmes +date: Mon Aug 25 19:06:28 2008 +0900 +description: +encode string as utf-8 before sending to latex (fix win32) + +changeset: 702:222fee895606 +user: Damien Elmes +date: Mon Aug 25 17:52:49 2008 +0900 +description: +bump version + +changeset: 701:beae5d21dcff +user: Damien Elmes +date: Mon Aug 25 15:03:45 2008 +0900 +description: +since we're randomizing field ids, delete local fields before syncing + +changeset: 700:9be61e41abc6 +user: Damien Elmes +date: Mon Aug 25 14:15:26 2008 +0900 +description: +fix bug adding cards with priorities + +changeset: 699:26c2fd9803b3 +user: Damien Elmes +date: Mon Aug 25 01:52:10 2008 +0900 +description: +catch interrupted system calls on osx (fix latex) + +changeset: 698:b00e8943896e +user: Damien Elmes +date: Mon Aug 25 00:59:32 2008 +0900 +description: +bump version number + +changeset: 697:72877d4c65ac +user: Damien Elmes +date: Sun Aug 24 23:49:28 2008 +0900 +description: +fix for old python + +changeset: 696:1dbfe6cf704f +user: Damien Elmes +date: Sun Aug 24 23:46:24 2008 +0900 +description: +catch error when latex not available + +changeset: 695:849dd0cb1e66 +user: Damien Elmes +date: Sun Aug 24 20:34:05 2008 +0900 +description: +check missing fields, reset isDue on check, force random field ids + +changeset: 694:a92453fe34fa +user: Damien Elmes +date: Sun Aug 24 16:08:46 2008 +0900 +description: +add fns to check deck integrity and optimize. backup before upgrade + +changeset: 693:40d7c642effe +user: Damien Elmes +date: Sun Aug 24 15:12:56 2008 +0900 +description: +flush before deleting + +changeset: 692:2ef84a67a27f +user: Damien Elmes +date: Sun Aug 24 14:20:18 2008 +0900 +description: +rebuild all due cards on upgrade + +changeset: 691:1cc8b894488c +user: Damien Elmes +date: Sat Aug 23 13:24:35 2008 +0900 +description: +fix typo + +changeset: 690:f1ad96b4ccdf +user: Damien Elmes +date: Sat Aug 23 13:20:45 2008 +0900 +description: +same for single priority + +changeset: 689:447e0ef9f5a2 +user: Damien Elmes +date: Sat Aug 23 13:20:17 2008 +0900 +description: +don't mark card modified when updating priorities + +changeset: 688:c07dc999a451 +user: Damien Elmes +date: Sat Aug 23 13:01:07 2008 +0900 +description: +fix for suspending cards + +changeset: 687:a1730621b2fa +user: Damien Elmes +date: Sat Aug 23 05:52:13 2008 +0900 +description: +in latex code keep win32 code win32 only + +changeset: 686:5351ab8ffe26 +user: Damien Elmes +date: Sat Aug 23 05:45:09 2008 +0900 +description: +create media dir in latex + +changeset: 685:abdfdb8d8259 +user: Damien Elmes +date: Fri Aug 22 18:36:31 2008 +0900 +description: +bump version + +changeset: 684:d8e21f20ea13 +user: Damien Elmes +date: Fri Aug 22 18:23:12 2008 +0900 +description: +ease=0 -> interval=1, ensure relativeDelay is defined properly for vhp cards + +changeset: 683:210ac919afba +user: Damien Elmes +date: Fri Aug 22 17:22:46 2008 +0900 +description: +rebuild type on updatePriority() too + +changeset: 682:0743d48a5f6a +user: Damien Elmes +date: Fri Aug 22 17:17:27 2008 +0900 +description: +rebuild types when priorities change + +changeset: 681:562e0b7121e2 +user: Damien Elmes +date: Fri Aug 22 17:09:17 2008 +0900 +description: +switch priority direction on new cards, fix indexes, upgrade deck + +changeset: 680:630332d5b52e +user: Damien Elmes +date: Fri Aug 22 17:08:05 2008 +0900 +description: +card.toDB(): calculate relative delay based on new interval, not existing + +changeset: 679:47b11a72d93e +user: Damien Elmes +date: Fri Aug 22 13:52:30 2008 +0900 +description: +encode texpath in file system encoding before calling latex + +changeset: 678:8b7e242c5c2c +user: Damien Elmes +date: Fri Aug 22 04:10:36 2008 +0900 +description: +make latex python 2.4 compatible + +changeset: 677:6e5256cca4ba +user: Damien Elmes +date: Fri Aug 22 03:38:11 2008 +0900 +description: +update translations + +changeset: 676:90385ecde4a2 +user: Damien Elmes +date: Fri Aug 22 03:37:36 2008 +0900 +description: +revert to new sync proto + +changeset: 675:cdff208f7750 +user: Damien Elmes +date: Fri Aug 22 03:25:31 2008 +0900 +description: +hide dos box on win32, use file system encoding on latex file + +changeset: 674:98c6d7176e58 +user: Damien Elmes +date: Fri Aug 22 03:01:39 2008 +0900 +description: +use subprocess for latex generation, check cached image files better + +changeset: 673:b1e5281cc468 +user: Damien Elmes +date: Fri Aug 22 02:11:45 2008 +0900 +description: +catch latex/dvipng error messages + +changeset: 672:9d9a66c4cc4c +user: Damien Elmes +date: Fri Aug 22 01:27:43 2008 +0900 +description: +add uniqueness check for media dir + +changeset: 671:d8412e943854 +user: Damien Elmes +date: Fri Aug 22 00:39:01 2008 +0900 +description: +update kanji stats for libanki + +changeset: 670:3279d36b3873 +user: Damien Elmes +date: Thu Aug 21 16:49:56 2008 +0900 +description: +remove trailing

from finished msg + +changeset: 669:4194bf5ab584 +user: Damien Elmes +date: Thu Aug 21 03:02:05 2008 +0900 +description: +add old sync code back in until ready to release + +changeset: 668:15057e5bfe96 +user: Damien Elmes +date: Thu Aug 21 02:57:57 2008 +0900 +description: +when rounding, round to point + +changeset: 667:e3eecc872bd9 +user: Damien Elmes +date: Wed Aug 20 23:51:42 2008 +0900 +description: +fix resetCard(), add deck finished msg + +changeset: 666:d10fe7cf68db +user: Damien Elmes +date: Wed Aug 20 22:33:42 2008 +0900 +description: +increase number of backups, allow new deck directory to be customized + +changeset: 665:0944063a48a9 +user: Damien Elmes +date: Wed Aug 20 15:23:11 2008 +0900 +description: +put preSyncRefresh() in correct place + +changeset: 664:8de167986624 +user: Damien Elmes +date: Tue Aug 19 00:40:21 2008 +0900 +description: +use native mac audio + +changeset: 663:5f3c43b8e09e +user: Damien Elmes +date: Tue Aug 19 00:36:33 2008 +0900 +description: +make sure to rebuild priorities for client after sync + +changeset: 662:a11e5e138913 +user: Damien Elmes +date: Mon Aug 18 17:39:36 2008 +0900 +description: +catch missing files when queueing + +changeset: 661:4c84f8f41e29 +user: Damien Elmes +date: Mon Aug 18 14:56:52 2008 +0900 +description: +avoid division by zero in markExpiredCardsDue() + +changeset: 660:7909069b4b98 +user: Damien Elmes +date: Mon Aug 18 14:34:58 2008 +0900 +description: +fix previous change + +changeset: 659:4a9cebb03bbb +user: Damien Elmes +date: Mon Aug 18 14:12:11 2008 +0900 +description: +no need to update _dailyStats, bundle daily stats without orm + +changeset: 658:0c41b35558b8 +user: Damien Elmes +date: Mon Aug 18 14:03:57 2008 +0900 +description: +update stats pre-sync + +changeset: 657:af0319d44b8d +user: Damien Elmes +date: Fri Aug 15 21:11:28 2008 +0900 +description: +make upgrade more robust, fix sync path + +changeset: 656:a7525fc35edd +user: Damien Elmes +date: Fri Aug 15 17:20:23 2008 +0900 +description: +typo in getCardIds(), don't sync new deck values yet + +changeset: 655:701156cd121f +parent: 651:5d4b6f4cfeef +user: Damien Elmes +date: Fri Aug 15 16:36:48 2008 +0900 +description: +set _countsDirty on open, add getCards(), update updateAllPriorities() + +changeset: 654:c55cd3992387 +parent: 651:5d4b6f4cfeef +parent: 653:4a530339560c +user: Damien Elmes +date: Thu Aug 14 15:48:30 2008 +0900 +description: +merge + +changeset: 653:4a530339560c +user: Damien Elmes +date: Thu Aug 14 15:45:39 2008 +0900 +description: +don't send version + +changeset: 652:f772a8c41ed8 +parent: 643:577fc8703e3e +user: Damien Elmes +date: Thu Aug 14 15:34:23 2008 +0900 +description: +use an explicit table name to avoid problems accessing old decks + +changeset: 651:5d4b6f4cfeef +user: Damien Elmes +date: Thu Aug 14 15:43:59 2008 +0900 +description: +don't send version number, as it's handled locally + +changeset: 650:19bb0dfba67f +user: Damien Elmes +date: Thu Aug 14 15:21:17 2008 +0900 +description: +remove debugging + +changeset: 649:03b8972f5bc7 +user: Damien Elmes +date: Thu Aug 14 15:04:50 2008 +0900 +description: +when rolling back the deck, make sure to clear the session + +changeset: 648:0959736b0b6f +user: Damien Elmes +date: Thu Aug 14 13:49:51 2008 +0900 +description: +add final review support, cardsDueBy(), check due + 1 due to integer precision + +changeset: 647:14ee60a8146c +user: Damien Elmes +date: Wed Aug 06 14:35:09 2008 +0900 +description: +refactor failed card handling + +changeset: 646:0a3f138ce999 +user: Damien Elmes +date: Sun Aug 03 16:08:55 2008 +0900 +description: +update function names + +changeset: 645:3e8f51d39c5c +user: Damien Elmes +date: Sun Aug 03 15:54:20 2008 +0900 +description: +refactor stats code, counts, getCard + +changeset: 644:8de7f216a2eb +parent: 642:e1198518d82f +parent: 643:577fc8703e3e +user: Damien Elmes +date: Sun Jul 27 20:15:35 2008 +0900 +description: +merge with stable + +changeset: 643:577fc8703e3e +parent: 638:d9a10b14042f +user: Damien Elmes +date: Sun Jul 27 20:14:56 2008 +0900 +description: +add temporary hack to prevent obscure problem with web interface + +changeset: 642:e1198518d82f +user: Damien Elmes +date: Sun Jul 27 15:54:57 2008 +0900 +description: +more sql queue work. update stats and syncing too + +changeset: 641:3f7c2ec8f9af +user: Damien Elmes +date: Mon Jul 21 01:24:19 2008 +0900 +description: +update stats code for sql only version + +changeset: 640:4bedac110b09 +user: Damien Elmes +date: Sun Jul 20 16:15:52 2008 +0900 +description: +further enhancements to scheduling algo, remove old code + +changeset: 639:54f572ab7574 +user: Damien Elmes +date: Fri Jul 18 23:03:15 2008 +0900 +description: +new scheduling algorithm, getCard/answerCard->non-orm, upgrade deck to v1 +- queue implemented using standard sql statements and indexes +- flush() removed from statement() in db.py +- rewrite getCard() and answerCard() to support pure sql +- move index definitions into deck code, and update all at once +- upgrade deck to v1, use new file format, add relativeDelay/isDue + +changeset: 638:d9a10b14042f +user: Damien Elmes +date: Sat Jul 12 14:03:49 2008 +0900 +description: +change spacing method + +changeset: 637:0a4493ceba9a +user: Damien Elmes +date: Mon Jul 07 23:46:37 2008 +0900 +description: +bump version number + +changeset: 636:0dd97215713f +user: Damien Elmes +date: Mon Jul 07 23:29:24 2008 +0900 +description: +update korean translations + +changeset: 635:666a61a1005d +user: Damien Elmes +date: Sun Jul 06 17:56:37 2008 +0900 +description: +bump version number + +changeset: 634:41c8719dea8f +user: Damien Elmes +date: Mon Jun 30 13:27:17 2008 +0900 +description: +don't run psyco in sync tests + +changeset: 633:76bfad36edeb +user: Damien Elmes +date: Mon Jun 30 12:39:56 2008 +0900 +description: +1 days -> 1 day + +changeset: 632:d42cbcdb9ca0 +user: Damien Elmes +date: Mon Jun 23 18:30:44 2008 +0900 +description: +analyze DB on open to fix slow query bug + +changeset: 631:0a207d41bdde +user: Damien Elmes +date: Mon Jun 23 17:59:01 2008 +0900 +description: +always check spacing, even if failed + +changeset: 630:3e72d2bb72cd +user: Damien Elmes +date: Thu Jun 12 13:14:59 2008 +0900 +description: +remove hashbangs from scripts + +changeset: 629:5adc70f26434 +user: Damien Elmes +date: Mon Jun 09 12:59:15 2008 +0900 +description: +use small size latex and support utf8 +patch from ancechu on the mnemosyne forums + +changeset: 628:d16b7864fb51 +user: Damien Elmes +date: Sun Jun 08 20:06:48 2008 +0900 +description: +add more allowed characters, change pysqlite order + +changeset: 627:24f7f67ec3be +user: Damien Elmes +date: Sat May 24 15:51:43 2008 +0900 +description: +updated korean translations + +changeset: 626:27621df9a3bf +user: Damien Elmes +date: Sat May 24 15:29:24 2008 +0900 +description: +bump version number + +changeset: 625:a421ba21ad3b +user: Damien Elmes +date: Sat May 24 13:06:35 2008 +0900 +description: +fix problem with plural forms + +changeset: 624:abfe8cc9e157 +user: Damien Elmes +date: Sat May 24 13:01:11 2008 +0900 +description: +add Korean translation from Jin Eundeok + +changeset: 623:d7dd97ccaec9 +user: Damien Elmes +date: Wed May 21 16:11:27 2008 +0900 +description: +update valid deck chars + +changeset: 622:ce2ca629c3ab +user: Damien Elmes +date: Mon May 19 20:13:16 2008 +0900 +description: +don't assume the thread-local variables have been initialized in other threads + +changeset: 621:3f2424f85055 +user: Damien Elmes +date: Mon May 19 20:08:08 2008 +0900 +description: +make language handling thread-local + +changeset: 620:274acd4864a3 +user: Damien Elmes +date: Mon May 19 18:31:20 2008 +0900 +description: +strip bad characters from sync name + +changeset: 619:1aafaf051652 +user: Damien Elmes +date: Mon May 19 13:11:10 2008 +0900 +description: +open unihan db session on each reading request, to work in threaded apps + +changeset: 618:552a9a5c2b66 +user: Damien Elmes +date: Sun May 18 19:43:19 2008 +0900 +description: +add missing file from previous commit + +changeset: 617:a114cda5b4fa +user: Damien Elmes +date: Sun May 18 19:34:44 2008 +0900 +description: +add cuecard importer from chris aakre + +changeset: 616:51fd2d028f19 +user: Damien Elmes +date: Sun May 18 16:23:24 2008 +0900 +description: +add strip latex support + +changeset: 615:e616d09f0347 +user: Damien Elmes +date: Sun May 18 15:22:06 2008 +0900 +description: +calculate start of date based on gmtime, not local time + +changeset: 614:6506d6a30683 +user: Damien Elmes +date: Sun May 18 15:07:53 2008 +0900 +description: +setup.py: gplv3->gplv3 + +changeset: 613:e521fe622380 +user: Damien Elmes +date: Mon Apr 07 17:28:45 2008 +0900 +description: +don't accidently create models as we try to delete them + +changeset: 612:358abd1adcf0 +user: Damien Elmes +date: Thu Apr 03 12:42:53 2008 +0900 +description: +use different cutoff in final drill, bump version + +changeset: 611:317d90474379 +user: Damien Elmes +date: Wed Apr 02 22:39:29 2008 +0900 +description: +bump version number + +changeset: 610:6b2b0dbfa5d4 +user: Damien Elmes +date: Tue Apr 01 12:29:05 2008 +0900 +description: +don't throw away failed cards if not due yet, add collapsedFailedCards() + +changeset: 609:fafbd0f3017c +user: Damien Elmes +date: Mon Mar 31 11:16:57 2008 +0900 +description: +set cwd to tmpdir when generating latex + +changeset: 608:bae31e9e2016 +user: Damien Elmes +date: Mon Mar 31 11:12:00 2008 +0900 +description: +define mature cards as currentInterval >= 21, fix cardState() + +changeset: 607:33b75850cc13 +user: Damien Elmes +date: Mon Mar 31 10:47:11 2008 +0900 +description: +when adding spaced cards back on future queue, make sure to convert to future item + +changeset: 606:08d922f58e7a +user: Damien Elmes +date: Mon Mar 24 15:56:55 2008 +0900 +description: +fix path separator + +changeset: 605:73eb316d6a38 +user: Damien Elmes +date: Mon Mar 24 04:14:47 2008 +0900 +description: +try to load graphs twice (fixes graph bug on unicode names on win32) + +changeset: 604:f64adfd3f64e +user: Damien Elmes +date: Mon Mar 24 03:26:39 2008 +0900 +description: +bump version number + +changeset: 603:0fb38c00c31b +user: Damien Elmes +date: Mon Mar 24 02:44:43 2008 +0900 +description: +enforce priority order suspended -> high -> med -> low -> norm + +changeset: 602:5e235ccdcc85 +user: Damien Elmes +date: Mon Mar 24 02:26:45 2008 +0900 +description: +commit() manually on export, remove redundant flush in anki03 and deck + +changeset: 601:e03056c908c7 +user: Damien Elmes +date: Wed Mar 19 15:20:21 2008 +0900 +description: +gpl2 -> gpl3 + +changeset: 600:a8e67b62d6e6 +user: Damien Elmes +date: Wed Mar 19 15:18:21 2008 +0900 +description: +add /usr/texbin on osx + +changeset: 599:16d967dcf64b +user: Damien Elmes +date: Wed Mar 19 15:15:16 2008 +0900 +description: +support copying media dir on saveas + +changeset: 598:3aba7dd81593 +user: Damien Elmes +date: Wed Mar 19 12:00:23 2008 +0900 +description: +in refresh(), flush any changes then reload changes after session attach + +changeset: 597:ec65c140f655 +user: Damien Elmes +date: Sat Mar 15 13:07:20 2008 +0900 +description: +catch database is locked as well as table is locked + +changeset: 596:d1a64da72c7d +user: Damien Elmes +date: Wed Mar 12 13:26:36 2008 +0900 +description: +remove debug statement + +changeset: 595:a8905a6cb733 +user: Damien Elmes +date: Wed Mar 12 13:25:36 2008 +0900 +description: +remove rebuild deck on finish code, since spacing is no longer an issue + +changeset: 594:6383725e5fcd +user: Damien Elmes +date: Wed Mar 12 13:24:22 2008 +0900 +description: +only update spacing if > than before, set default spacing of 10% + +changeset: 593:7bbc82d1b84c +user: Damien Elmes +date: Sun Mar 09 10:30:06 2008 +0900 +description: +check to see if earliesttime is valid + +changeset: 592:73ca94c12b43 +user: Damien Elmes +date: Sun Mar 09 03:09:55 2008 +0900 +description: +typo + +changeset: 591:114c9307d70e +user: Damien Elmes +date: Sun Mar 09 03:09:36 2008 +0900 +description: +rebuild queue if cards are due + +changeset: 590:a79ae9c52011 +user: Damien Elmes +date: Sun Mar 09 02:59:18 2008 +0900 +description: +ver=0.9.5.4 + +changeset: 589:af29531578b7 +user: Damien Elmes +date: Sun Mar 09 02:44:33 2008 +0900 +description: +ignore deleted cards/models/etc when importing + +changeset: 588:380b73a48552 +user: Damien Elmes +date: Sun Mar 09 01:21:33 2008 +0900 +description: +remove incomplete dutch translation + +changeset: 587:abd56386612f +user: Damien Elmes +date: Sat Mar 08 02:34:37 2008 +0900 +description: +fix export field order + +changeset: 586:e87a0eb0215e +user: Damien Elmes +date: Sat Mar 08 02:27:26 2008 +0900 +description: +don't apply distinct to field values on fact export + +changeset: 585:62a49cc767d2 +user: Damien Elmes +date: Sat Mar 08 02:01:03 2008 +0900 +description: +genID in normal import, too + +changeset: 584:87a748693258 +user: Damien Elmes +date: Sat Mar 08 01:59:54 2008 +0900 +description: +genID() on anki03 import + +changeset: 583:ca7b58b3ee04 +user: Damien Elmes +date: Sat Mar 08 00:35:40 2008 +0900 +description: +correctly handle failed cards not due yet in final review & failed cards count + +changeset: 582:3fc290930148 +user: Damien Elmes +date: Fri Mar 07 23:57:53 2008 +0900 +description: +put tex file in tmp dir too + +changeset: 581:166e30a6fa56 +user: Damien Elmes +date: Tue Mar 04 00:08:49 2008 +0900 +description: +typo + +changeset: 580:b492ced5d846 +user: Damien Elmes +date: Tue Mar 04 00:07:57 2008 +0900 +description: +update pendingFailed/etc + +changeset: 579:523f4063003a +user: Damien Elmes +date: Mon Mar 03 23:48:34 2008 +0900 +description: +include modified in props, get by oldest modified for final review too + +changeset: 578:3dc73bf9e57a +user: Damien Elmes +date: Mon Mar 03 23:27:38 2008 +0900 +description: +if->elif + +changeset: 577:ce3c685f27db +user: Damien Elmes +date: Mon Mar 03 23:12:01 2008 +0900 +description: +failed cards -> failed queue + +changeset: 576:ab762131e1db +user: Damien Elmes +date: Mon Mar 03 22:46:47 2008 +0900 +description: +ver=0.9.5.3 + +changeset: 575:3dff276a3146 +user: Damien Elmes +date: Mon Mar 03 22:45:12 2008 +0900 +description: +update ES translation + +changeset: 574:4bdaff4f8fdc +user: Damien Elmes +date: Mon Mar 03 20:01:21 2008 +0900 +description: +refactor scheduling code to address some more problems & fix many problems +- build queue from a single sql call with type identifier instead of four +separate views. this greatly reduces the complexity of the sql statements and +removes the possibility of a card appearing in more than one queue +- store all failed cards in the failed cards queue, regardless of due time. +this reduces the complexity of the above and some other parts of the code +- when pulling items from the failed queue due to maxFailed reached, get the +oldest modified item instead of oldest due. this ensures that cards are +removed in the order they were added and it's not possible for a '0' answer to +be placed in front of all '1' answers. +- don't apply spacing to failed cards on either fetch or add operations +- catch only locking errors when opening a deck; re-raise non-locking errors +- catch locking errors on open as well as lock operations + +changeset: 573:f9b0f65540ad +user: Damien Elmes +date: Fri Feb 29 15:07:49 2008 +0900 +description: +remove card cache and flush changes to db instead + +changeset: 572:0216b0bf690d +user: Damien Elmes +date: Fri Feb 29 02:05:46 2008 +0900 +description: +bump version number + +changeset: 571:1f138784d8da +user: Damien Elmes +date: Fri Feb 29 02:01:37 2008 +0900 +description: +address possible off-by-one in failed/future distinction + +changeset: 570:edaa11cd4969 +user: Damien Elmes +date: Fri Feb 29 00:32:46 2008 +0900 +description: +when calculating earliest due, factor in collapseTime + +changeset: 569:62ceaca5da85 +user: Damien Elmes +date: Fri Feb 29 00:26:42 2008 +0900 +description: +filter tags for facts too on export + +changeset: 568:b092c2b4a44e +user: Damien Elmes +date: Fri Feb 29 00:02:50 2008 +0900 +description: +update french translations, bump version number + +changeset: 567:dba74965ea8a +user: Damien Elmes +date: Thu Feb 28 23:30:18 2008 +0900 +description: +make sure to update lastCardId, and keep cache of cards + +changeset: 566:515ab9e71d5b +user: Damien Elmes +date: Thu Feb 28 05:48:02 2008 +0900 +description: +encode backup dir too before generating backup path + +changeset: 565:4d45506e825c +user: Damien Elmes +date: Thu Feb 28 03:40:17 2008 +0900 +description: +add interrupted system call workaround + +changeset: 564:528144c8861e +user: Damien Elmes +date: Thu Feb 28 03:28:15 2008 +0900 +description: +bump version number + +changeset: 563:cf9f25c19e7d +user: Damien Elmes +date: Thu Feb 28 03:26:07 2008 +0900 +description: +fix addFact properly + +changeset: 562:4dd909b79cff +user: Damien Elmes +date: Thu Feb 28 03:13:01 2008 +0900 +description: +fix deck unit tests + +changeset: 561:e67f8d23ece9 +user: Damien Elmes +date: Thu Feb 28 03:08:59 2008 +0900 +description: +save or update fact when adding + +changeset: 560:1f76f7d8aa2a +user: Damien Elmes +date: Thu Feb 28 01:47:23 2008 +0900 +description: +remove target deck before saveas + +changeset: 559:d68d668561ad +user: Damien Elmes +date: Thu Feb 28 01:45:26 2008 +0900 +description: +remove debug statement + +changeset: 558:b7ad9d9501fd +user: Damien Elmes +date: Thu Feb 28 01:36:42 2008 +0900 +description: +saveas support + +changeset: 557:997d2acce31a +user: Damien Elmes +date: Thu Feb 28 00:29:13 2008 +0900 +description: +kakasi: preserve newlines when editing (catch
) + +changeset: 556:c5d42e49521a +user: Damien Elmes +date: Thu Feb 28 00:18:52 2008 +0900 +description: +reset spacing on export, set mod time + +changeset: 555:948b50693325 +user: Damien Elmes +date: Thu Feb 28 00:16:33 2008 +0900 +description: +when resetting cards, reset spacing too, and set modtime + +changeset: 554:30c0b7f61fda +user: Damien Elmes +date: Thu Feb 28 00:01:18 2008 +0900 +description: +fix put spaced cards in correct queue, pull failed cards from future + +changeset: 553:b5b3a7410f31 +user: Damien Elmes +date: Thu Feb 28 00:00:37 2008 +0900 +description: +add first answered to card stats + +changeset: 552:a2cf6fd896a0 +user: Damien Elmes +date: Tue Feb 26 17:44:49 2008 +0900 +description: +failed -> not failed, add resetCards support + +changeset: 551:a56b18dd983a +user: Damien Elmes +date: Tue Feb 26 16:46:41 2008 +0900 +description: +fix eta for new cards + +changeset: 550:954568afa53d +user: Damien Elmes +date: Mon Feb 25 17:16:46 2008 +0900 +description: +add delete empty models support + +changeset: 549:e6f6a5160173 +user: Damien Elmes +date: Mon Feb 25 16:47:04 2008 +0900 +description: +update translations + +changeset: 548:e8ab6027a118 +user: Damien Elmes +date: Mon Feb 25 16:15:43 2008 +0900 +description: +support adding tags to anki10, set mod on anki10 + +changeset: 547:3f2b1b17cfe2 +user: Damien Elmes +date: Mon Feb 25 15:28:36 2008 +0900 +description: +update all cards with the current card model id, not all card models ids + +changeset: 546:8f362e457134 +user: Damien Elmes +date: Mon Feb 25 14:01:59 2008 +0900 +description: +set default factor=2.5 on export + +changeset: 545:7d627396ae19 +user: Damien Elmes +date: Sun Feb 24 22:22:46 2008 +0900 +description: +don't collect low priority cards if they are failed + +changeset: 544:94a60c418ec5 +user: Damien Elmes +date: Sat Feb 23 19:56:51 2008 +0900 +description: +if all cards are failed, make sure spacing is at least delay0/delay1 + +changeset: 543:a9d488cecde1 +user: Damien Elmes +date: Sat Feb 23 19:04:06 2008 +0900 +description: +fix low priority + +changeset: 542:69fb21c53b18 +user: Damien Elmes +date: Sat Feb 23 18:45:46 2008 +0900 +description: +since sqlite doesn't have subsecond accuracy, add one to future queue + +changeset: 541:236ec293ac76 +user: Damien Elmes +date: Sat Feb 23 18:05:33 2008 +0900 +description: +update translations + +changeset: 540:c5d1d46e092a +user: Damien Elmes +date: Sat Feb 23 18:02:55 2008 +0900 +description: +put high priority new cards in rev queue too, fix getstats to understand + +changeset: 539:a503ff0fb920 +user: Damien Elmes +date: Sat Feb 23 15:53:14 2008 +0900 +description: +add more debugging info + +changeset: 538:697a6aa6eec2 +user: Damien Elmes +date: Sat Feb 23 15:35:58 2008 +0900 +description: +don't take out transaction in object_session, close deck on export + +changeset: 537:7cec96fa045e +user: Damien Elmes +date: Sat Feb 23 14:20:32 2008 +0900 +description: +always open a session, whether we're locking or not + +changeset: 536:a48e8869d944 +user: Damien Elmes +date: Sat Feb 23 14:04:34 2008 +0900 +description: +change transaction handling + +changeset: 535:6391564d0a59 +user: Damien Elmes +date: Fri Feb 22 23:55:58 2008 +0900 +description: +include created in items + +changeset: 534:0ae8a9276a60 +user: Damien Elmes +date: Fri Feb 22 22:37:17 2008 +0900 +description: +sort 'deck order' new cards by created, not due + +changeset: 533:54bb37136335 +user: Damien Elmes +date: Fri Feb 22 22:31:34 2008 +0900 +description: +add time to due delay before comparison + +changeset: 532:977950ea60b5 +user: Damien Elmes +date: Fri Feb 22 22:18:42 2008 +0900 +description: +take out a write lock after every save + +changeset: 531:cb5fb49ff027 +user: Damien Elmes +date: Fri Feb 22 22:00:35 2008 +0900 +description: +update firstAnswered on answer + +changeset: 530:6b6cb6032d24 +user: Damien Elmes +date: Fri Feb 22 04:38:37 2008 +0900 +description: +scheduling changes to address a few bugs +- store only soon due cards in the failed queue. other cards go into the +future queue +- addExpiredItem() chooses future or failed as necessary +- remove failedDueSoon() as the length of the failed queue will suffice now +- include failed, non-due cards in future queue +- only use max(card.due, facts.spaceUntil) on a different card, instead of +indiscriminately +- log non-due cards showing up in queue + +changeset: 529:8beac072db96 +user: Damien Elmes +date: Fri Feb 22 02:47:37 2008 +0900 +description: +don't recount ease2 in total + +changeset: 528:6ec2e64f543a +user: Damien Elmes +date: Fri Feb 22 01:45:52 2008 +0900 +description: +fix stats mislayout, and debugging info temporarily + +changeset: 527:9f6634c55f74 +user: Damien Elmes +date: Thu Feb 21 23:28:56 2008 +0900 +description: +output dvipng text to log file, too + +changeset: 526:6efe614e5783 +user: Damien Elmes +date: Thu Feb 21 00:59:32 2008 +0900 +description: +treat spaced cards as not due in graphs + +changeset: 525:7032963c45e3 +user: Damien Elmes +date: Wed Feb 20 21:39:01 2008 +0900 +description: +bump version number + +changeset: 524:1ed1bf270ec0 +user: Damien Elmes +date: Wed Feb 20 21:36:35 2008 +0900 +description: +fix win32 dying on utime + +changeset: 523:9aebd3f33208 +user: Damien Elmes +date: Wed Feb 20 21:34:43 2008 +0900 +description: +importing: set mod, inc timestamp on every card, set reps in mnemosyne import + +changeset: 522:111a89dc1a7d +user: Damien Elmes +date: Wed Feb 20 20:57:40 2008 +0900 +description: +add back accidently removed spacedCardCount() + +changeset: 521:9b7ea500e6d6 +user: Damien Elmes +date: Wed Feb 20 19:52:15 2008 +0900 +description: +flush before checking earliest, updated sql pending counts, off by 1, space hack + +changeset: 520:1a51d20d0c67 +user: Damien Elmes +date: Wed Feb 20 18:21:21 2008 +0900 +description: +future queue shouldn't order by ordinal + +changeset: 519:09b7354d9054 +user: Damien Elmes +date: Wed Feb 20 18:06:36 2008 +0900 +description: +fix case sensitivity problems with priorities/tags + +changeset: 518:4786ab9c7d92 +user: Damien Elmes +date: Wed Feb 20 18:05:02 2008 +0900 +description: +fix 8 hours->10 minutes (again), report keyerror on missing field in fact + +changeset: 517:cd2445e7315a +user: Damien Elmes +date: Wed Feb 20 02:08:11 2008 +0900 +description: +always use heap for acq, order by id on random, fix priorities + +changeset: 516:3cdad305b8c6 +user: Damien Elmes +date: Tue Feb 19 01:46:34 2008 +0900 +description: +make sure failed/successive reflects currentCard + +changeset: 515:7b3852e8467d +user: Damien Elmes +date: Tue Feb 19 01:39:15 2008 +0900 +description: +typo + +changeset: 514:45d61ec90048 +user: Damien Elmes +date: Tue Feb 19 01:29:16 2008 +0900 +description: +insert and sort instead + +changeset: 513:e45ead4b23e8 +user: Damien Elmes +date: Tue Feb 19 01:27:57 2008 +0900 +description: +ensure random new cards show in order + +changeset: 512:97e6d8c6db61 +user: Damien Elmes +date: Tue Feb 19 00:27:56 2008 +0900 +description: +fetch all cards into revision queue, not earliest ordinal + +changeset: 511:9ff150cf14b1 +user: Damien Elmes +date: Mon Feb 18 23:59:28 2008 +0900 +description: +support mnemosyne version 2 decks + +changeset: 510:33141dcabac4 +user: Damien Elmes +date: Mon Feb 18 20:15:25 2008 +0900 +description: +add failed/successive + +changeset: 509:7b784852048e +user: Damien Elmes +date: Mon Feb 18 17:00:54 2008 +0900 +description: +ease2 = yes + +changeset: 508:ccf03fd550f8 +user: Damien Elmes +date: Sun Feb 17 21:38:51 2008 +0900 +description: +calculate nextDue based on old state + +changeset: 507:07b4a2308008 +user: Damien Elmes +date: Sun Feb 17 21:31:40 2008 +0900 +description: +>= not > in failedCardMax, fix thinko in seen fact repression + +changeset: 506:2c53ed770cc3 +user: Damien Elmes +date: Sun Feb 17 04:32:13 2008 +0900 +description: +calculate nextDue with old state + +changeset: 505:6260e8d0b8c8 +user: Damien Elmes +date: Sun Feb 17 02:25:30 2008 +0900 +description: +fix win32 backup problem, syncing deleting everything + +changeset: 504:51f78cb0a6c9 +user: Damien Elmes +date: Sun Feb 17 01:25:20 2008 +0900 +description: +ensure priorities and suspended are case-insensitive + +changeset: 503:7c9f6826d07a +user: Damien Elmes +date: Sat Feb 16 23:24:20 2008 +0900 +description: +bump version number + +changeset: 502:673df6435b3d +user: Damien Elmes +date: Sat Feb 16 23:14:15 2008 +0900 +description: +ensure utf8 is passed to sqlite, don't show suspended in sql card counts + +changeset: 501:ab2eb6518847 +user: Damien Elmes +date: Sat Feb 16 21:55:01 2008 +0900 +description: +fix encoding issues + +changeset: 500:c77088668277 +user: Damien Elmes +date: Sat Feb 16 04:47:27 2008 +0900 +description: +update translations + +changeset: 499:0190a25e8116 +user: Damien Elmes +date: Sat Feb 16 04:40:56 2008 +0900 +description: +update version number, sync URL + +changeset: 498:aa0c093177a7 +user: Damien Elmes +date: Sat Feb 16 04:38:22 2008 +0900 +description: +don't decrement on failed cards, failedDueSoon, fix suspended&acqCards + +changeset: 497:804b745cb613 +user: Damien Elmes +date: Fri Feb 15 23:58:19 2008 +0900 +description: +do spaced check on all queues, not just future + +changeset: 496:b85b94f522aa +user: Damien Elmes +date: Fri Feb 15 15:07:08 2008 +0900 +description: +fix old stats when not in final review + +changeset: 495:d941ef5c2568 +user: Damien Elmes +date: Thu Feb 14 01:36:31 2008 +0900 +description: +add some docs to __init__ + +changeset: 494:9b1b2e078d3b +user: Damien Elmes +date: Thu Feb 14 01:21:31 2008 +0900 +description: +update stats to reflect factor, update lastFactor on answer + +changeset: 493:721fa0fa5e23 +user: Damien Elmes +date: Thu Feb 14 01:09:24 2008 +0900 +description: +add last factor to card, factor&last to history + +changeset: 492:f5f140bcf875 +user: Damien Elmes +date: Wed Feb 13 23:44:23 2008 +0900 +description: +test for sqlite + +changeset: 491:86f46df15879 +user: Damien Elmes +date: Wed Feb 13 23:28:28 2008 +0900 +description: +improve simplejson version check + +changeset: 490:35b9b7175062 +user: Damien Elmes +date: Wed Feb 13 22:58:41 2008 +0900 +description: +fix delete card tags + +changeset: 489:659435efb87c +user: Damien Elmes +date: Wed Feb 13 22:58:27 2008 +0900 +description: +fix add card tags + +changeset: 488:d5a0287d0c34 +user: Damien Elmes +date: Wed Feb 13 22:02:17 2008 +0900 +description: +remove echo=false + +changeset: 487:d4bc57e5080a +user: Damien Elmes +date: Wed Feb 13 21:55:33 2008 +0900 +description: +another attempt + +changeset: 486:e7f5fe8e3a83 +user: Damien Elmes +date: Wed Feb 13 21:42:58 2008 +0900 +description: +print deck error to stderr + +changeset: 485:e1d29d5e0ef2 +user: Damien Elmes +date: Wed Feb 13 21:28:26 2008 +0900 +description: +another attempt at unicodetext compat + +changeset: 484:f4c4c2248244 +user: Damien Elmes +date: Wed Feb 13 16:35:30 2008 +0900 +description: +import text + +changeset: 483:a54a25860f08 +user: Damien Elmes +date: Wed Feb 13 02:39:56 2008 +0900 +description: +don't do anything if no cards to update on card model change + +changeset: 482:59d58a57f176 +user: Damien Elmes +date: Wed Feb 13 02:39:06 2008 +0900 +description: +refactor sql renderqa into cardmodel, add q/a update on card model change + +changeset: 481:73716aab572a +user: Damien Elmes +date: Wed Feb 13 01:48:15 2008 +0900 +description: +bugfixes in priorities, syncing, and saving & oldCardCount +- ensure simplejson 1.7 or more +- only update card priorities if changed, and set mod time +- make oldCardCount report all non-new cards +- don't set modified on save, should already be modified +- remove reference to json2 +- only update cards on fact change if textChanged=True +- flush card and field models on add, to ensure subsequent delete works +- lastSync = 0 if not same on server and client +- bulk delete of facts and cards in sync +- fix createDeck command in sync + +changeset: 480:d8d15968be41 +user: Damien Elmes +date: Mon Feb 11 23:47:59 2008 +0900 +description: +fix field order, add initial spacing + +changeset: 479:064f2ff34327 +user: Damien Elmes +date: Mon Feb 11 18:13:14 2008 +0900 +description: +limit final review to collapseTime, bump version number + +changeset: 478:b9dcd6f217f1 +user: Damien Elmes +date: Mon Feb 11 17:56:40 2008 +0900 +description: +add new card spacing attr for later, rebuild queue on empty, add fuzz to spacing + +changeset: 477:bffa023c72e1 +user: Damien Elmes +date: Mon Feb 11 15:30:46 2008 +0900 +description: +preserve model created/mod, use same cmodel/fmodel id, preserve modtime on upgrade + +changeset: 476:e180bf592812 +user: Damien Elmes +date: Mon Feb 11 14:58:11 2008 +0900 +description: +add model test + +changeset: 475:89025cabb354 +user: Damien Elmes +date: Mon Feb 11 14:56:47 2008 +0900 +description: +pending card count when queue not built, getstats uses currentcard, sync fix + +changeset: 474:18f6e364e443 +user: Damien Elmes +date: Fri Feb 08 21:03:23 2008 +0900 +description: +add close method, add optional backup/locking, make anki03 import safer + +changeset: 473:28108e81b9c8 +user: Damien Elmes +date: Wed Feb 06 23:19:25 2008 +0900 +description: +move to unique identifiers, use simplejson, remove unique name checks + +changeset: 472:2f864d898a9c +user: Damien Elmes +date: Mon Feb 04 19:07:46 2008 +0900 +description: +bump version number + +changeset: 471:6beca934d4e1 +user: Damien Elmes +date: Mon Feb 04 17:45:35 2008 +0900 +description: +dispose of engine on failure (fix win32 bug) + +changeset: 470:fd2dea923ea1 +user: Damien Elmes +date: Mon Feb 04 17:32:48 2008 +0900 +description: +fix excessive /, only add views if necessary, lock db, typo + +changeset: 469:911cfe0f3bd4 +user: Damien Elmes +date: Mon Feb 04 16:56:15 2008 +0900 +description: +move new deck path into separate routine + +changeset: 468:7334806dfa95 +user: Damien Elmes +date: Mon Feb 04 16:52:43 2008 +0900 +description: +reimplement backup support, use mtime instead of diff + +changeset: 467:6d8a803a1098 +user: Damien Elmes +date: Mon Feb 04 15:20:52 2008 +0900 +description: +index factId on cards table, add numeric attr in field model + +changeset: 466:71d87d9ff488 +user: Damien Elmes +date: Mon Feb 04 13:14:20 2008 +0900 +description: +handle case where min or avg is None + +changeset: 465:f57e4be7a78d +user: Damien Elmes +date: Mon Feb 04 11:30:21 2008 +0900 +description: +add spaced card count + +changeset: 464:4d2fcc3d168c +user: Damien Elmes +date: Mon Feb 04 11:26:50 2008 +0900 +description: +fix typo + +changeset: 463:22fe19136e06 +user: Damien Elmes +date: Mon Feb 04 11:16:48 2008 +0900 +description: +relative spacing + +changeset: 462:7f6ec59d2dc7 +user: Damien Elmes +date: Sun Feb 03 14:38:48 2008 +0900 +description: +speed up model deletion + +changeset: 461:ab7b909ccc67 +user: Damien Elmes +date: Sun Feb 03 02:49:09 2008 +0900 +description: +fix unit tests updating test files + +changeset: 460:7199bba3220c +user: Damien Elmes +date: Sun Feb 03 02:35:10 2008 +0900 +description: +fix invalid numbers in importing + +changeset: 459:80e403561c46 +user: Damien Elmes +date: Sun Feb 03 02:28:36 2008 +0900 +description: +add all updated files + +changeset: 458:e659844c57ab +user: Damien Elmes +date: Sun Feb 03 02:23:39 2008 +0900 +description: +support field count greater than models when importing, fix bugs + +changeset: 457:574c48bbdda3 +user: Damien Elmes +date: Sun Feb 03 01:44:34 2008 +0900 +description: +ensure we add empty fields too + +changeset: 456:2c87169fe867 +user: Damien Elmes +date: Sun Feb 03 00:53:59 2008 +0900 +description: +anki10 import support + +changeset: 455:89ff10c2dcc6 +user: Damien Elmes +date: Sun Feb 03 00:17:19 2008 +0900 +description: +change select order, add index to field model and value + +changeset: 454:a2b67f3dbac9 +user: Damien Elmes +date: Sun Feb 03 00:16:03 2008 +0900 +description: +fix uniqueness check looking at other fields + +changeset: 453:83bdeeb05b90 +user: Damien Elmes +date: Sat Feb 02 22:47:48 2008 +0900 +description: +csv/mnemosyne/anki03 importers working + +changeset: 452:996c42126688 +user: Damien Elmes +date: Fri Feb 01 20:01:28 2008 +0900 +description: +fix graphs on empty, media dir, locked db, remove factorChange + +changeset: 451:fd5d0dd0ecac +user: Damien Elmes +date: Fri Feb 01 15:55:06 2008 +0900 +description: +more scheduling tweaks, fix q/a bug + +changeset: 450:a7a162b15bfc +user: Damien Elmes +date: Fri Feb 01 14:50:13 2008 +0900 +description: +change scheduling algo, fix sql bug + +changeset: 449:9ba84487d601 +user: Damien Elmes +date: Thu Jan 31 23:57:17 2008 +0900 +description: +newcardplacement -> newcardorder + +changeset: 448:156b41a3a1cb +user: Damien Elmes +date: Thu Jan 31 23:30:18 2008 +0900 +description: +show kanji stats only for seen cards + +changeset: 447:3277f9ea9574 +user: Damien Elmes +date: Thu Jan 31 23:21:25 2008 +0900 +description: +reorganise importing + +changeset: 446:739a127bc72a +user: Damien Elmes +date: Thu Jan 31 23:21:12 2008 +0900 +description: +only save if modified, and after saving ensure lastLoaded = modified + +changeset: 445:ccf18729eb2f +user: Damien Elmes +date: Wed Jan 30 15:29:23 2008 +0900 +description: +UnicodeText compat fix, fix broken statement() + +changeset: 444:86628769a647 +user: Damien Elmes +date: Wed Jan 30 14:59:20 2008 +0900 +description: +add export tags support + +changeset: 443:7156e0b1243b +user: Damien Elmes +date: Wed Jan 30 14:38:43 2008 +0900 +description: +count facts in export, fix html formatting & kakasi bug + +changeset: 442:6495344038d3 +user: Damien Elmes +date: Wed Jan 30 03:29:04 2008 +0900 +description: +fix stats reporting wrong remaining number + +changeset: 441:407b7177336b +user: Damien Elmes +date: Wed Jan 30 02:04:06 2008 +0900 +description: +finish bulk tag update routines + +changeset: 440:d16f8c4205c0 +user: Damien Elmes +date: Wed Jan 30 01:32:32 2008 +0900 +description: +add bulk card/fact delete, start of tags, make rebuild optional + +changeset: 439:c883b0fbf4bd +user: Damien Elmes +date: Tue Jan 29 01:53:54 2008 +0900 +description: +cache question/answer in card, finish exporting (much faster) + +changeset: 438:97c1168e0f8e +user: Damien Elmes +date: Mon Jan 28 23:49:47 2008 +0900 +description: +fix sqlalchemy depreciation, implement card export + +changeset: 437:3f060c6ae690 +user: Damien Elmes +date: Mon Jan 28 19:50:07 2008 +0900 +description: +export anki support + +changeset: 436:021d5b32d76b +user: Damien Elmes +date: Mon Jan 28 03:05:02 2008 +0900 +description: +fix oldcardcount/newcardcount, stats on new deck + +changeset: 435:8bb62308395b +user: Damien Elmes +date: Mon Jan 28 02:38:47 2008 +0900 +description: +fix required/unique on import, fix unit test other->basic + +changeset: 434:708512162d38 +user: Damien Elmes +date: Mon Jan 28 01:48:28 2008 +0900 +description: +convert chinese pickle support to db + +changeset: 433:3e2a7d96b470 +user: Damien Elmes +date: Mon Jan 28 01:37:59 2008 +0900 +description: +add tests, chinese (move to db) + +changeset: 432:ff11bf6084f0 +user: Damien Elmes +date: Mon Jan 28 01:37:29 2008 +0900 +description: +cleanup cards.py + +changeset: 431:3b12714f6ade +user: Damien Elmes +date: Mon Jan 28 01:08:43 2008 +0900 +description: +remote sync implemented + +changeset: 430:4a2f588ea119 +user: Damien Elmes +date: Mon Jan 28 00:16:12 2008 +0900 +description: +add alignment back + +changeset: 429:00ce16b7bb8c +user: Damien Elmes +date: Sun Jan 27 23:03:13 2008 +0900 +description: +fix latex + +changeset: 428:6815007aa990 +user: Damien Elmes +date: Sun Jan 27 20:55:54 2008 +0900 +description: +be sure to flush deletion and update deck when deleting models + +changeset: 427:02853198c573 +user: Damien Elmes +date: Sat Jan 26 18:08:05 2008 +0900 +description: +fix matplotlib error, remove references to fields on delete, fix unique bug + +changeset: 426:2e8356b7b8ed +user: Damien Elmes +date: Fri Jan 25 20:10:28 2008 +0900 +description: +revamp tag utils + +changeset: 425:7ca343a58e65 +user: Damien Elmes +date: Fri Jan 25 01:11:20 2008 +0900 +description: +add missing sync cases, add/delete/ fieldmodels/cardmodels, enable lastSync + +changeset: 424:472dde78f1d0 +user: Damien Elmes +date: Tue Jan 22 23:52:05 2008 +0900 +description: +facts implemented in pure sql + +changeset: 423:8abe57b723bf +user: Damien Elmes +date: Tue Jan 22 23:30:23 2008 +0900 +description: +convert card syncing to pure sql + +changeset: 422:821cb86e23a3 +user: Damien Elmes +date: Tue Jan 22 01:36:21 2008 +0900 +description: +implemented card/fact syncing - see notes +- lastSync = 0 for now, needs more thinking +- cards/facts syncing very slow due to orm overhead, needs to be rewriten in +pure sql + +changeset: 421:5bf940a20645 +user: Damien Elmes +date: Mon Jan 21 15:22:24 2008 +0900 +description: +half of syncing implemented, various changes to facts/etc for syncing +- only send ids changed later than lastSync +- bundle information into a payload to decrease latency +- factor latex into sourcecode (insecure in deck) +- implement history tracking +- uniquify field/model/cardmodel/fieldmodel ids + +changeset: 420:b63ad96e01aa +user: Damien Elmes +date: Fri Jan 18 20:22:20 2008 +0900 +description: +update czech translation, refactor + +changeset: 419:57a17dddcb19 +user: Damien Elmes +date: Thu Jan 17 21:45:41 2008 +0900 +description: +after 3 days, add more delay at half speed + +changeset: 418:a83d8d225dff +user: Damien Elmes +date: Thu Jan 17 01:33:32 2008 +0900 +description: +we can skip the factId sort + +changeset: 417:d9b57fbafc58 +user: Damien Elmes +date: Thu Jan 17 01:19:32 2008 +0900 +description: +ordinals, features, unicode +- store card model order in cards +- get the first available card in order, to ensure cards are shown in time +- do it in python, as it's about 3-4x faster than the equivalent sql +- decorators -> features +- add tests for features and stdmodels +- add unicode wrappers for various data that may not be unicode +- only add first card in a set in addCardToQueue() + +changeset: 416:51b85f619590 +user: Damien Elmes +date: Wed Jan 16 19:24:14 2008 +0900 +description: +factor sql expressions into views, fix remaining stdmodels + +changeset: 415:f702768fa05f +user: Damien Elmes +date: Tue Jan 15 01:24:25 2008 +0900 +description: +fix graphs + +changeset: 414:241fc522b9ba +user: Damien Elmes +date: Tue Jan 15 00:50:36 2008 +0900 +description: +implement remaining stats, fix deck.created in import, add deck predicates + +changeset: 413:52dffc6d4d41 +user: Damien Elmes +date: Mon Jan 14 18:17:54 2008 +0900 +description: +fix bug in distracted time calculation + +changeset: 412:86d266baa213 +user: Damien Elmes +date: Mon Jan 14 18:06:05 2008 +0900 +description: +priority queue scheduling, implemented stats generation + +changeset: 411:512e30710a77 +user: Damien Elmes +date: Sun Jan 13 17:01:27 2008 +0900 +description: +max new cards feature, refactor getCards() + +changeset: 410:881ecf95a7e8 +user: Damien Elmes +date: Thu Jan 10 11:39:45 2008 +0900 +description: +track fact/card/deck modtime + +changeset: 409:8b764de79c57 +user: Damien Elmes +date: Thu Jan 10 10:48:36 2008 +0900 +description: +deleting cards/facts + +changeset: 408:7fb0cdb42a59 +user: Damien Elmes +date: Thu Jan 10 09:06:26 2008 +0900 +description: +work on scheduling + +changeset: 407:95dce2fd08f3 +user: Damien Elmes +date: Mon Jan 07 14:36:26 2008 +0900 +description: +initial work on sql backend + +changeset: 406:7a9bd84316df +user: Damien Elmes +date: Fri Jan 04 00:53:47 2008 +0900 +description: +add hack for cardIsNew() and old clients + +changeset: 405:8db50f3515d1 +user: Damien Elmes +date: Thu Jan 03 23:08:10 2008 +0900 +description: +remove ineffective auto priority update code + +changeset: 404:01bac1c9867f +user: Damien Elmes +date: Thu Jan 03 22:10:42 2008 +0900 +description: +store total separately + +changeset: 403:ffbfd576c3f0 +user: Damien Elmes +date: Thu Jan 03 05:03:54 2008 +0900 +description: +bump version number + +changeset: 402:b3432c758826 +user: Damien Elmes +date: Thu Jan 03 05:02:10 2008 +0900 +description: +don't dirty cards when changing priority + +changeset: 401:8eb4d13a6ea8 +user: Damien Elmes +date: Thu Jan 03 02:52:14 2008 +0900 +description: +update priority in sched, but only if deck is assigned + +changeset: 400:c03dee6ab5b9 +user: Damien Elmes +date: Thu Jan 03 02:42:17 2008 +0900 +description: +upgrade scheduler, and then deck + +changeset: 399:a45146c88c67 +user: Damien Elmes +date: Thu Jan 03 02:15:50 2008 +0900 +description: +bump version number + +changeset: 398:b11217f19d93 +user: Damien Elmes +date: Wed Jan 02 21:42:08 2008 +0900 +description: +add czech translation + +changeset: 397:07c36434a501 +user: Damien Elmes +date: Wed Jan 02 21:23:01 2008 +0900 +description: +repose.cx -> ichi2.net + +changeset: 396:d990980bd410 +user: Damien Elmes +date: Mon Dec 31 18:56:50 2007 +0900 +description: +change tag priority handling, double speed of getcards() + +changeset: 395:7d4965319532 +user: Damien Elmes +date: Fri Dec 28 07:20:29 2007 +0900 +description: +report 8 hours not 10 minutes if necessary + +changeset: 394:d92e3dda8637 +user: Damien Elmes +date: Thu Dec 27 18:18:42 2007 +0900 +description: +kill kakasi path on ppc + +changeset: 393:51634574a50b +user: Damien Elmes +date: Thu Dec 27 18:16:50 2007 +0900 +description: +look in a different location for unihan.pickle on mac + +changeset: 392:cb165be012ad +user: Damien Elmes +date: Thu Dec 27 16:13:05 2007 +0900 +description: +add new properties to sync + +changeset: 391:5a36d3042797 +user: Damien Elmes +date: Thu Dec 27 15:55:17 2007 +0900 +description: +change delay2 to 8 hours, fix logic reversal + +changeset: 390:68631c8e6da6 +user: Damien Elmes +date: Thu Dec 27 15:44:44 2007 +0900 +description: +if failed to create media dir (due to read only), return none + +changeset: 389:79f13bd847c4 +user: Damien Elmes +date: Thu Dec 27 15:32:15 2007 +0900 +description: +configurable collapse time + +changeset: 388:9f5514ca9f59 +user: Damien Elmes +date: Thu Dec 27 15:27:52 2007 +0900 +description: +add separate ease1 delay for mature cards + +changeset: 387:8e6f9afbfb0b +user: Damien Elmes +date: Wed Dec 26 02:35:03 2007 +0900 +description: +don't strip html from kakasi (allow multi-line furigana) + +changeset: 386:08f6e556af4c +user: Damien Elmes +date: Tue Dec 25 23:45:04 2007 +0900 +description: +update 'a short time' jp translation + +changeset: 385:82181513047d +user: Damien Elmes +date: Tue Dec 25 22:56:02 2007 +0900 +description: +update jp translations + +changeset: 384:7e2552ceb812 +user: Damien Elmes +date: Tue Dec 25 04:48:12 2007 +0900 +description: +fix all the unit tests broken by the move to a single card 'other' model + +changeset: 383:2fd4209bcbc2 +user: Damien Elmes +date: Sat Dec 22 17:39:40 2007 +0900 +description: +add 'tags' to list of available fields to display + +changeset: 382:fc82511d9ba2 +user: Damien Elmes +date: Sat Dec 22 05:37:28 2007 +0900 +description: +fix pending# calculation for final drill + +changeset: 381:74596ce9f43d +user: Damien Elmes +date: Sat Dec 22 05:22:11 2007 +0900 +description: +change wording of ease 0/1 when in final drill + +changeset: 380:801e3783cbe3 +user: Damien Elmes +date: Sat Dec 22 05:12:36 2007 +0900 +description: +add support for 'final review' instead of making people wait 10 minutes + +changeset: 379:f621b76e0218 +user: Damien Elmes +date: Sat Dec 22 03:59:46 2007 +0900 +description: +make 'other' default to only front->back, remove unnecessary standard models + +changeset: 378:ca51a4ed83fb +user: Damien Elmes +date: Sat Dec 22 02:47:00 2007 +0900 +description: +simplify insertion order to random/append, don't sort new cards + +changeset: 377:e4a6fd9dc58c +user: Damien Elmes +date: Sat Dec 22 02:18:20 2007 +0900 +description: +remove redundant repositioning code + +changeset: 376:6dc270145078 +user: Damien Elmes +date: Sat Dec 22 02:05:15 2007 +0900 +description: +update heisig link in stdmodels + +changeset: 375:9bc6c376d88e +user: Damien Elmes +date: Sat Dec 22 01:34:36 2007 +0900 +description: +send deleted cards in summary, fix syncing problems related to deletion + +changeset: 374:ce502f16b31b +user: Damien Elmes +date: Fri Dec 21 23:02:39 2007 +0900 +description: +don't touch original deck when exporting as .anki (fix export bug) + +changeset: 373:8c7b78bf3ee1 +user: Damien Elmes +date: Fri Dec 21 22:42:15 2007 +0900 +description: +use 3.1 format for months, full path to custom json + +changeset: 372:c29bf4d7ca02 +user: Damien Elmes +date: Tue Nov 13 18:28:04 2007 +0900 +description: +add support for 'medium priority' + +changeset: 371:a4270575763a +user: Damien Elmes +date: Mon Nov 12 16:18:08 2007 +0900 +description: +add latex support + +changeset: 370:3824f1270bce +user: Damien Elmes +date: Mon Nov 12 14:37:25 2007 +0900 +description: +report delay0/1 instead of subsequent interval in nextIntervalStr() + +changeset: 369:5dea85b15a5d +user: Damien Elmes +date: Mon Nov 12 13:42:20 2007 +0900 +description: +tweak deck stats definitions + +changeset: 368:1cb413b81471 +user: Damien Elmes +date: Mon Nov 12 13:35:40 2007 +0900 +description: +fix font/color problem for elements with space, remove debug statement + +changeset: 367:fe8943934270 +user: Damien Elmes +date: Mon Nov 12 13:24:57 2007 +0900 +description: +8-12hr hardInterval, support months, non day interval pairs + +changeset: 366:39cdf33dcc6d +user: Damien Elmes +date: Mon Nov 12 12:00:33 2007 +0900 +description: +set nextTime to lastTime in makeDue() to ensure same priority + +changeset: 365:56aaf97fcfdb +user: Damien Elmes +date: Wed Oct 31 13:35:05 2007 +0900 +description: +update french translations, fix some translation bugs + +changeset: 364:0f0cea8f35e6 +user: Damien Elmes +date: Sat Oct 20 03:18:19 2007 +0900 +description: +use a relative delay instead of partitioning young/mature + +changeset: 363:58fa58aa3c55 +user: Damien Elmes +date: Sat Oct 20 01:09:25 2007 +0900 +description: +experimental scheduling order + +changeset: 362:4ca091d8de4a +user: Damien Elmes +date: Mon Sep 24 16:04:06 2007 +0900 +description: +allow longs in json sync + +changeset: 361:5088b5186091 +user: Damien Elmes +date: Fri Sep 07 22:42:49 2007 +0900 +description: +update translations, add more french work from laurent steffan + +changeset: 360:221cfff61e91 +user: Damien Elmes +date: Fri Sep 07 22:40:35 2007 +0900 +description: +add tag indicating dupe field when importing + +changeset: 359:01f4097dc1b5 +user: Damien Elmes +date: Fri Sep 07 22:23:45 2007 +0900 +description: +bump version number + +changeset: 358:0aa83bdb1217 +user: Damien Elmes +date: Fri Sep 07 21:46:19 2007 +0900 +description: +update heisig deck's link to koohii + +changeset: 357:047d3802f109 +user: Damien Elmes +date: Fri Sep 07 21:45:05 2007 +0900 +description: +make sure to give the deck an abspath when loading + +changeset: 356:b050ae539dc7 +user: Damien Elmes +date: Fri Sep 07 20:57:39 2007 +0900 +description: +typo in importing, catch sound playing errors + +changeset: 355:269d7357bacf +user: Damien Elmes +date: Thu Sep 06 05:08:07 2007 +0900 +description: +add anki v.3 support + +changeset: 354:7af926127391 +user: Damien Elmes +date: Thu Sep 06 04:00:44 2007 +0900 +description: +add final newline + +changeset: 353:dd2165b06a36 +user: Damien Elmes +date: Thu Sep 06 03:44:27 2007 +0900 +description: +polish exporting + +changeset: 352:42ee46f2b201 +user: Damien Elmes +date: Thu Sep 06 00:56:52 2007 +0900 +description: +catch socket errors in sync + +changeset: 351:6b36b8d35f15 +user: Damien Elmes +date: Thu Sep 06 00:09:46 2007 +0900 +description: +write to a temp file when saving + +changeset: 350:7e15eae29926 +user: Damien Elmes +date: Wed Sep 05 23:51:57 2007 +0900 +description: +don't have to worry about cross-device links as we're saving to the config dir + +changeset: 349:66bc87cfe523 +user: Damien Elmes +date: Wed Sep 05 23:42:22 2007 +0900 +description: +don't rename, copy media files (as the old deck should remain valid) + +changeset: 348:76522ca69016 +user: Damien Elmes +date: Wed Sep 05 22:39:46 2007 +0900 +description: +rename media dir on save + +changeset: 347:b9b47b806cb6 +user: Damien Elmes +date: Wed Sep 05 21:26:46 2007 +0900 +description: +don't try and create media dir if no path set + +changeset: 346:5e9125d08f32 +user: Damien Elmes +date: Tue Aug 28 20:29:28 2007 +0900 +description: +preliminary exporting support + +changeset: 345:de191312e9bc +user: Damien Elmes +date: Tue Aug 28 03:34:03 2007 +0900 +description: +ignore suspended cards when generating graphs + +changeset: 344:77ae56436668 +user: Damien Elmes +date: Mon Aug 27 13:05:52 2007 +0900 +description: +import mnemosyne stats properly + +changeset: 343:18164de78cde +user: Damien Elmes +date: Sat Aug 25 04:40:16 2007 +0900 +description: +bump version + +changeset: 342:402d229a13ba +user: Damien Elmes +date: Fri Aug 24 22:28:07 2007 +0900 +description: +add data for 0 days too (fixes due graph and cumulative graph) + +changeset: 341:fbd08aaf2a71 +user: Damien Elmes +date: Fri Aug 24 22:23:06 2007 +0900 +description: +pygame expects bytestrings for a path, not unicode + +changeset: 340:d704cbd8188d +user: Damien Elmes +date: Thu Aug 23 22:55:10 2007 +0900 +description: +bump version number + +changeset: 339:90d50e5c7ee9 +user: Damien Elmes +date: Thu Aug 23 22:52:47 2007 +0900 +description: +create correct media dir if file is .fc too + +changeset: 338:416c63dd91d5 +user: Damien Elmes +date: Thu Aug 23 02:45:46 2007 +0900 +description: +import mnemosyne categories, change sound tags + +changeset: 337:75078a7bce21 +user: Damien Elmes +date: Thu Aug 23 02:11:17 2007 +0900 +description: +update translations, add dutch + +changeset: 336:8df1e9b1970b +user: Damien Elmes +date: Thu Aug 23 00:07:52 2007 +0900 +description: +add optional backup when loading + +changeset: 335:63c0d62ebc7d +user: Damien Elmes +date: Thu Aug 23 00:01:43 2007 +0900 +description: +open the files in binary when checking backups (stupid windows) + +changeset: 334:af46ee8f7a7b +user: Damien Elmes +date: Wed Aug 22 23:41:15 2007 +0900 +description: +fix typo + +changeset: 333:0cd4dfa7bf5a +user: Damien Elmes +date: Wed Aug 22 23:40:39 2007 +0900 +description: +add warning about pygame + +changeset: 332:16adc1d6f27b +user: Damien Elmes +date: Wed Aug 22 23:40:24 2007 +0900 +description: +don't play if not available + +changeset: 331:f906b9a96b6b +user: Damien Elmes +date: Wed Aug 22 23:25:11 2007 +0900 +description: +add sound support + +changeset: 330:b225c9302681 +user: Damien Elmes +date: Wed Aug 22 20:59:51 2007 +0900 +description: +media support + +changeset: 329:e02897454dcb +user: Damien Elmes +date: Wed Aug 22 18:19:49 2007 +0900 +description: +only use production by default in english model + +changeset: 328:1af88008a56f +user: Damien Elmes +date: Mon Aug 20 13:14:35 2007 +0900 +description: +don't take max(3) if less than 3 days + +changeset: 327:b7ee2b5485b3 +user: Damien Elmes +date: Mon Aug 20 13:11:01 2007 +0900 +description: +update scheduling estimate to reflect new delay handling + +changeset: 326:171967dc58fe +user: Damien Elmes +date: Sat Aug 18 14:30:36 2007 +0900 +description: +round days + +changeset: 325:f8c1c42f6bcb +user: Damien Elmes +date: Sat Aug 18 01:20:39 2007 +0900 +description: +use proper locale names instead of shorthand + +changeset: 324:19cae311245a +user: Damien Elmes +date: Sat Aug 18 00:35:40 2007 +0900 +description: +handle 0 day_s_ and plural forms for cards in initial state + +changeset: 323:61ce0aceee04 +user: Damien Elmes +date: Fri Aug 17 23:59:03 2007 +0900 +description: +return unknown if no cards are pending + +changeset: 322:1ba04daeb598 +user: Damien Elmes +date: Fri Aug 17 23:49:20 2007 +0900 +description: +remove filter, do it a different way + +changeset: 321:d82c13cdd265 +user: Damien Elmes +date: Fri Aug 17 22:57:35 2007 +0900 +description: +add filter support for getcard + +changeset: 320:927d8d784f24 +user: Damien Elmes +date: Fri Aug 17 22:11:18 2007 +0900 +description: +detect and remove unicode marker + +changeset: 319:7c470a3ba224 +user: Damien Elmes +date: Fri Aug 17 21:50:12 2007 +0900 +description: +reset pending if counts go below 0 + +changeset: 318:6091e5a9f1a4 +user: Damien Elmes +date: Fri Aug 17 20:30:02 2007 +0900 +description: +allow up to 3 days delay before halving + +changeset: 317:ace3a6db338b +user: Damien Elmes +date: Fri Aug 17 20:28:57 2007 +0900 +description: +halve delay boost for cards in initial state + +changeset: 316:68ea6eff400d +user: Damien Elmes +date: Fri Aug 17 20:27:44 2007 +0900 +description: +ease 2 levels down one, adjust factors as well + +changeset: 315:1f45fdda5488 +user: Damien Elmes +date: Thu Aug 16 03:37:25 2007 +0900 +description: +fix stats + +changeset: 314:a39af7438d8f +user: Damien Elmes +date: Wed Aug 15 03:22:01 2007 +0900 +description: +fix win32 path + +changeset: 313:786ba09d0c14 +user: Damien Elmes +date: Wed Aug 15 02:41:51 2007 +0900 +description: +put kakasi search in support/japanese + +changeset: 312:9d0b3ea8bb88 +user: Damien Elmes +date: Wed Aug 15 00:06:26 2007 +0900 +description: +spanish translations change + +changeset: 311:41552532fa85 +user: Damien Elmes +date: Tue Aug 14 23:22:20 2007 +0900 +description: +tweak a buggy fuzzy match + +changeset: 310:4df5234d6b07 +user: Damien Elmes +date: Tue Aug 14 23:18:11 2007 +0900 +description: +add spanish support + +changeset: 309:322c11a7fdba +user: Damien Elmes +date: Tue Aug 14 23:16:24 2007 +0900 +description: +add spanish translation + +changeset: 308:0062da597de7 +user: Damien Elmes +date: Tue Aug 14 23:11:48 2007 +0900 +description: +compare using only id, not modified, cope with missing card links on del + +changeset: 307:34507c39cef3 +user: Damien Elmes +date: Tue Aug 14 04:46:55 2007 +0900 +description: +add de translation + +changeset: 306:e23f6671b396 +user: Damien Elmes +date: Tue Aug 14 00:24:16 2007 +0900 +description: +add french translation + +changeset: 305:b5e16bf218d1 +user: Damien Elmes +date: Mon Aug 13 19:34:00 2007 +0900 +description: +accept multiple tags when adding/removing + +changeset: 304:c302b44ea197 +user: Damien Elmes +date: Mon Aug 13 10:52:29 2007 +0900 +description: +bundle locale files in egg + +changeset: 303:68e18840d5ee +user: Damien Elmes +date: Mon Aug 13 07:24:39 2007 +0900 +description: +fix plural handling, more translation updates + +changeset: 302:dfd61a2a3b2d +user: Damien Elmes +date: Mon Aug 13 06:17:40 2007 +0900 +description: +give libanki the same version number, so that they can be matched together + +changeset: 301:17a1c50cffce +user: Damien Elmes +date: Mon Aug 13 05:29:42 2007 +0900 +description: +move json2 into correct place, update setup.py + +changeset: 300:3ccbd5ecf622 +user: Damien Elmes +date: Sun Aug 12 22:37:04 2007 +0900 +description: +locale tweaks + +changeset: 299:1d5b59c76a7d +user: Damien Elmes +date: Sun Aug 12 05:40:27 2007 +0900 +description: +strip html in chinese, too + +changeset: 298:29f277a46175 +user: Damien Elmes +date: Sun Aug 12 05:39:21 2007 +0900 +description: +strip html in kakasi + +changeset: 297:ad410a6a03f6 +user: Damien Elmes +date: Sun Aug 12 03:50:17 2007 +0900 +description: +don't escape html when rendering card model, since we do that on add + +changeset: 296:58ac3c478d57 +user: Damien Elmes +date: Sun Aug 12 01:28:14 2007 +0900 +description: +backup on load, too + +changeset: 295:1db906069868 +user: Damien Elmes +date: Sat Aug 11 02:38:23 2007 +0900 +description: +quote fonts + +changeset: 294:b58970d35289 +user: Damien Elmes +date: Fri Aug 10 21:25:26 2007 +0900 +description: +look for locale in alternate location (fixes win32 build) + +changeset: 293:1a089a863627 +user: Damien Elmes +date: Fri Aug 10 20:22:45 2007 +0900 +description: +add/del tag, resetcard/makedue, updatehistory changes + +changeset: 292:36e1ab8c001b +user: Damien Elmes +date: Fri Aug 10 19:12:50 2007 +0900 +description: +earliestTime shouldn't include suspended cards, add spaced check + +changeset: 291:084e94fc9fe7 +user: Damien Elmes +date: Fri Aug 10 16:41:02 2007 +0900 +description: +update the pending number every 10 cards at worst + +changeset: 290:44af96eafc88 +user: Damien Elmes +date: Fri Aug 10 16:32:26 2007 +0900 +description: +don't kill empty fields in import + +changeset: 289:0dc9bc7771b2 +user: Damien Elmes +date: Fri Aug 10 14:14:57 2007 +0900 +description: +fix multiple inheritence bug & comparison of facts + +changeset: 288:0361a855ee17 +user: Damien Elmes +date: Fri Aug 10 12:03:34 2007 +0900 +description: +don't delete facts either + +changeset: 287:7be29f8c82d0 +user: Damien Elmes +date: Fri Aug 10 11:49:58 2007 +0900 +description: +facts -> deck.facts + +changeset: 286:83b765a5c338 +user: Damien Elmes +date: Fri Aug 10 11:47:14 2007 +0900 +description: +when removing a model, don't delete cards/facts - that will be done later + +changeset: 285:e36c86704289 +user: Damien Elmes +date: Fri Aug 10 00:16:26 2007 +0900 +description: +fix translation switching, add a few translations + +changeset: 284:a4b5c5064681 +user: Damien Elmes +date: Thu Aug 09 04:22:09 2007 +0900 +description: +new sync url + +changeset: 283:9ef404d1b071 +user: Damien Elmes +date: Thu Aug 09 03:26:44 2007 +0900 +description: +change max # of backups to 15 + +changeset: 282:e24075724d1c +user: Damien Elmes +date: Thu Aug 09 03:10:20 2007 +0900 +description: +library version -> 0.3 + +changeset: 281:d03d0b1101f9 +user: Damien Elmes +date: Thu Aug 09 03:06:36 2007 +0900 +description: +don't make the 'meaning' part unique if it's a one-way deck + +changeset: 280:db6dcf2afaf7 +user: Damien Elmes +date: Wed Aug 08 19:50:43 2007 +0900 +description: +sched stats: mark failed cards in the old category, not the new + +changeset: 279:f7fc7372bae4 +user: Damien Elmes +date: Wed Aug 08 19:46:03 2007 +0900 +description: +if autosingle, rebuild list with enforced order in importing; ratio=1.7 + +changeset: 278:cf0e7ad95610 +user: Damien Elmes +date: Tue Aug 07 11:05:48 2007 +0900 +description: +intern strings when decoding from json + +changeset: 277:83f202d5edce +user: Damien Elmes +date: Tue Aug 07 09:39:26 2007 +0900 +description: +stop deck from doubling size on sync (don't coerce keys to unicode) + +changeset: 276:a3c49e99b509 +user: Damien Elmes +date: Mon Aug 06 16:53:57 2007 +0900 +description: +if cards < 2, don't change order + +changeset: 275:329b0ae99d1d +user: Damien Elmes +date: Mon Aug 06 07:34:04 2007 +0900 +description: +only show pending cards (non spaced-waiting), and 1.1 = plural + +changeset: 274:93a3a9f71ccc +user: Damien Elmes +date: Sun Aug 05 03:01:27 2007 +0900 +description: +uniquify fields list + +changeset: 273:8e87ff0b222e +user: Damien Elmes +date: Sun Aug 05 02:40:26 2007 +0900 +description: +don't attempt grouping on a single card model, allFields, fmtTimeSpan + +changeset: 272:98827f17ded6 +user: Damien Elmes +date: Sat Aug 04 20:51:37 2007 +0900 +description: +if no japanese text (and no english text), default to english + +changeset: 271:e733e0603bef +user: Damien Elmes +date: Sat Aug 04 15:08:13 2007 +0900 +description: +include time info in getstats, refactor fmttimediff, allow points + +changeset: 270:9ffaff6ed530 +user: Damien Elmes +date: Sat Aug 04 04:01:17 2007 +0900 +description: +handle py2exe when looking for unihan + +changeset: 269:0d065b819bf7 +user: Damien Elmes +date: Sat Aug 04 03:31:32 2007 +0900 +description: +convert japanese font names in linux too + +changeset: 268:0068328bbc98 +parent: 266:1da58c791cbd +parent: 267:b32498ba63ef +user: Damien Elmes +date: Sat Aug 04 03:17:10 2007 +0900 +description: +merge with other computer + +changeset: 267:b32498ba63ef +parent: 265:241131ae4230 +user: Damien Elmes +date: Sat Aug 04 02:06:41 2007 +0900 +description: +pass family verbatim if no platform names found + +changeset: 266:1da58c791cbd +user: Damien Elmes +date: Sat Aug 04 03:14:54 2007 +0900 +description: +support mandarin and cantonese + +changeset: 265:241131ae4230 +user: Damien Elmes +date: Sat Aug 04 01:52:25 2007 +0900 +description: +make standard models use canonical names + +changeset: 264:def6e4393656 +user: Damien Elmes +date: Sat Aug 04 01:45:03 2007 +0900 +description: +integrate font canonicalization with css generation, support html + +changeset: 263:f01502ee1c8b +user: Damien Elmes +date: Sat Aug 04 01:18:48 2007 +0900 +description: +generate substitution list + +changeset: 262:5fae82f760fe +user: Damien Elmes +date: Sat Aug 04 01:09:35 2007 +0900 +description: +add font canonicalization + +changeset: 261:4fdf114d761a +user: Damien Elmes +date: Sat Aug 04 00:09:51 2007 +0900 +description: +change kanji/hiragana/misc split method. should be a lot more accurate + +changeset: 260:72186e2a05a2 +user: Damien Elmes +date: Fri Aug 03 23:44:29 2007 +0900 +description: +change japanese text detection algo + +changeset: 259:2bafcdd7d327 +user: Damien Elmes +date: Fri Aug 03 23:33:26 2007 +0900 +description: +importing: guess single/multiple, behave more predictably regarding new cards + +changeset: 258:432edd7fe290 +user: Damien Elmes +date: Thu Aug 02 04:48:29 2007 +0900 +description: +update sample decks for new format + +changeset: 257:9976607ee619 +user: Damien Elmes +date: Thu Aug 02 04:03:00 2007 +0900 +description: +fix a number of syncing bugs, use ids not names + +changeset: 256:cdb1656069bc +user: Damien Elmes +date: Wed Aug 01 23:31:15 2007 +0900 +description: +refactor into idobj/list, fix syncing on win32 + +changeset: 255:601175f26251 +user: Damien Elmes +date: Wed Aug 01 17:29:43 2007 +0900 +description: +fix some unit tests + +changeset: 254:5c70723b63f2 +user: Damien Elmes +date: Wed Aug 01 12:29:42 2007 +0900 +description: +enforce unicode in save/load, update sync url + +changeset: 253:f71181fbd8b3 +user: Damien Elmes +date: Wed Aug 01 11:12:32 2007 +0900 +description: +preserve fact lastTags when importing + +changeset: 252:09555fed796e +user: Damien Elmes +date: Wed Aug 01 11:02:34 2007 +0900 +description: +include _ in cardmodels, fix reference to activatedCards in importing + +changeset: 251:e5b854ea295d +user: Damien Elmes +date: Wed Aug 01 10:40:43 2007 +0900 +description: +update sample decks for new format, remove some debugging code + +changeset: 250:4547a364e78b +user: Damien Elmes +date: Wed Aug 01 08:50:34 2007 +0900 +description: +factor models into separate class, track deletions, fix syncing + +changeset: 249:89be41543132 +user: Damien Elmes +date: Wed Aug 01 08:27:54 2007 +0900 +description: +decode user dir using file system encoding first + +changeset: 248:631f045a10f2 +user: Damien Elmes +date: Tue Jul 31 06:57:48 2007 +0900 +description: +don't update stats until after the card is scheduled (fixes new cards bug) + +changeset: 247:00fb7c13502b +user: Damien Elmes +date: Tue Jul 31 05:31:07 2007 +0900 +description: +set files r/w before trying to remove them, update model syncnames + +changeset: 246:b0f4fb55e2d6 +user: Damien Elmes +date: Tue Jul 31 05:17:31 2007 +0900 +description: +add jlpt sample decks again + +changeset: 245:3833b58982af +user: Damien Elmes +date: Tue Jul 31 04:25:40 2007 +0900 +description: +don't validate incoming facts when syncing + +changeset: 244:50688fef7f05 +user: Damien Elmes +date: Tue Jul 31 04:17:19 2007 +0900 +description: +bug in compat changes, change sync url + +changeset: 243:e6b894b2bd82 +user: Damien Elmes +date: Tue Jul 31 03:41:36 2007 +0900 +description: +more compat code + +changeset: 242:154711f215d8 +parent: 240:91c12216c2df +parent: 241:2ac34639bc08 +user: Damien Elmes +date: Tue Jul 31 02:22:21 2007 +0900 +description: +merge + +changeset: 241:2ac34639bc08 +parent: 234:29ea58becfde +user: Damien Elmes +date: Mon Jul 30 23:58:25 2007 +0900 +description: +encode the backup dir as unicode + +changeset: 240:91c12216c2df +user: Damien Elmes +date: Tue Jul 31 02:19:28 2007 +0900 +description: +make the old deck error a little nicer + +changeset: 239:2d6fa28f3080 +user: Damien Elmes +date: Tue Jul 31 02:10:04 2007 +0900 +description: +update sample decks + +changeset: 238:8786582a13f9 +user: Damien Elmes +date: Tue Jul 31 02:04:53 2007 +0900 +description: +keep track of the last fact tags used when adding + +changeset: 237:498d00566d67 +user: Damien Elmes +date: Tue Jul 31 01:51:24 2007 +0900 +description: +don't use disabled card models when importing + +changeset: 236:0e5c2236a41e +user: Damien Elmes +date: Tue Jul 31 01:30:47 2007 +0900 +description: +set the model to none if no models are available + +changeset: 235:c1b9f468a183 +user: Damien Elmes +date: Tue Jul 31 00:53:55 2007 +0900 +description: +fix a bug in syncing models + +changeset: 234:29ea58becfde +user: Damien Elmes +date: Mon Jul 30 23:20:02 2007 +0900 +description: +fix syncing of cardmodels, sync facts metadata too + +changeset: 233:04084a0a7dea +user: Damien Elmes +date: Sun Jul 29 15:12:33 2007 +0900 +description: +update some translations + +changeset: 232:99c78b9f034b +user: Damien Elmes +date: Sun Jul 29 14:42:03 2007 +0900 +description: +give cardmodels and fields an id + +changeset: 231:97dbc780c65a +user: Damien Elmes +date: Sat Jul 28 16:09:53 2007 +0900 +description: +correctly (don't) escape closing tags + +changeset: 230:ea63caae51fa +user: Damien Elmes +date: Sat Jul 28 02:01:20 2007 +0900 +description: +capitalize samples + +changeset: 229:55042d8edd2f +user: Damien Elmes +date: Sat Jul 28 00:55:21 2007 +0900 +description: +sync: diffs, don't update local time, conditional update, no syncName/name + +changeset: 228:35c866e7b50a +user: Damien Elmes +date: Fri Jul 27 21:18:36 2007 +0900 +description: +remove name properties from sample decks + +changeset: 227:acb500e68435 +user: Damien Elmes +date: Fri Jul 27 21:14:29 2007 +0900 +description: +remove reduntant 'name' field in deck, use filename instead + +changeset: 226:8b7c9e3a613a +user: Damien Elmes +date: Fri Jul 27 19:21:20 2007 +0900 +description: +add required fields to card model for later + +changeset: 225:2aa9dbb1c461 +user: Damien Elmes +date: Fri Jul 27 04:23:02 2007 +0900 +description: +remove min 1 day restriction + +changeset: 224:3e63913f3705 +user: Damien Elmes +date: Fri Jul 27 03:01:46 2007 +0900 +description: +only escape on html + +changeset: 223:8725632baf35 +user: Damien Elmes +date: Thu Jul 26 05:37:11 2007 +0900 +description: +don't assume globalstats exists, handle refs to anki.Deck + +changeset: 222:56e52010a122 +user: Damien Elmes +date: Thu Jul 26 04:58:41 2007 +0900 +description: +network sync working + +changeset: 221:7a15ae305f8c +user: Damien Elmes +date: Wed Jul 25 23:24:12 2007 +0900 +description: +update sample decks + +changeset: 220:bd1d20410180 +user: Damien Elmes +date: Wed Jul 25 22:36:40 2007 +0900 +description: +use field tags for defining which fields kakasi should operate on + +changeset: 219:be500ace46a0 +user: Damien Elmes +date: Wed Jul 25 12:11:36 2007 +0900 +description: +remove debugging + +changeset: 218:affe383805d0 +user: Damien Elmes +date: Wed Jul 25 11:40:21 2007 +0900 +description: +nextTime takes max of card/spacing, detect order of card when missing hiragana + +changeset: 217:362f694db9e9 +user: Damien Elmes +date: Tue Jul 24 04:05:27 2007 +0900 +description: +remove debugging code + +changeset: 216:bd28eddee8c2 +user: Damien Elmes +date: Tue Jul 24 04:02:02 2007 +0900 +description: +hackish escaping of html + +changeset: 215:a9a0b47a43a6 +user: Damien Elmes +date: Tue Jul 24 01:49:13 2007 +0900 +description: +fix bug in deleting fact on last card, guess en/ja when importing + +changeset: 214:54f2319774ed +user: Damien Elmes +date: Tue Jul 24 00:18:12 2007 +0900 +description: +simple->other in model names + +changeset: 213:5639e36cef42 +user: Damien Elmes +date: Mon Jul 23 23:43:05 2007 +0900 +description: +don't uniqify when importing, since that's o(n2). just tag + +changeset: 212:d8db8a7b090e +user: Damien Elmes +date: Mon Jul 23 23:35:18 2007 +0900 +description: +record delay in lastInterval, don't let missing fields prevent import + +changeset: 211:29aa042a9f74 +user: Damien Elmes +date: Mon Jul 23 23:22:51 2007 +0900 +description: +plural only on 1, not 0 + +changeset: 210:2f1471b58112 +user: Damien Elmes +date: Mon Jul 23 23:16:26 2007 +0900 +description: +convert \n to
on print html, check pending when 0 + +changeset: 209:20e66e556ce3 +user: Damien Elmes +date: Mon Jul 23 11:50:00 2007 +0900 +description: +make sure to update card models & unique/required when renaming a field + +changeset: 208:6af8399334d3 +user: Damien Elmes +date: Mon Jul 23 11:08:54 2007 +0900 +description: +catch incomplete model formats + +changeset: 207:c85b59f20e33 +user: Damien Elmes +date: Mon Jul 23 01:12:06 2007 +0900 +description: +remove : from backup paths because windows is stupid + +changeset: 206:34e76d7a05b8 +user: Damien Elmes +date: Mon Jul 23 00:28:37 2007 +0900 +description: +more backup fixes + +changeset: 205:d4b87b277f42 +user: Damien Elmes +date: Mon Jul 23 00:22:17 2007 +0900 +description: +bug in text field generation + +changeset: 204:cd0384f4b113 +user: Damien Elmes +date: Mon Jul 23 00:19:10 2007 +0900 +description: +backup bugs + +changeset: 203:749677a48442 +user: Damien Elmes +date: Mon Jul 23 00:11:48 2007 +0900 +description: +make heisig number required + +changeset: 202:f63a0722efcf +user: Damien Elmes +date: Sun Jul 22 23:47:07 2007 +0900 +description: +move samples to .anki format + +changeset: 201:4d75012163d9 +user: Damien Elmes +date: Sun Jul 22 23:29:43 2007 +0900 +description: +disable emacs support for now + +changeset: 200:5dc9ef2699bd +user: Damien Elmes +date: Sun Jul 22 21:20:22 2007 +0900 +description: +add russian example + +changeset: 199:86220210b75a +user: Damien Elmes +date: Sun Jul 22 20:49:45 2007 +0900 +description: +add heisig sample deck + +changeset: 198:c7745f756869 +user: Damien Elmes +date: Sun Jul 22 20:46:20 2007 +0900 +description: +add heisig sample deck + +changeset: 197:d8a6fe897168 +user: Damien Elmes +date: Sun Jul 22 20:13:47 2007 +0900 +description: +repositioning support, text question formats, add edit/last card for later + +changeset: 196:c82e5eaf7446 +user: Damien Elmes +date: Sun Jul 22 03:35:07 2007 +0900 +description: +misc tweaks&bugfixes: heisig, pending, qformat + +changeset: 195:1125c447b2d1 +user: Damien Elmes +date: Sun Jul 22 02:02:31 2007 +0900 +description: +multi-way model syncing, use local ids for server, not server's + +changeset: 194:3101e6529378 +user: Damien Elmes +date: Sun Jul 22 00:38:22 2007 +0900 +description: +finish card/fact syncing + +changeset: 193:cc04ec32fffc +user: Damien Elmes +date: Sat Jul 21 21:52:35 2007 +0900 +description: +syncing models implemented, bug in setModified, bug in sched(class variables) + +changeset: 192:e29f1efd37b7 +user: Damien Elmes +date: Sat Jul 21 02:55:29 2007 +0900 +description: +postponed->suspended + +changeset: 191:ac0ef2b43ac1 +user: Damien Elmes +date: Sat Jul 21 01:20:20 2007 +0900 +description: +postponing, case insensitive tags, field uniqueness fix + +changeset: 190:96aa1b8b9ac7 +user: Damien Elmes +date: Fri Jul 20 21:20:36 2007 +0900 +description: +add priorities in sched, refactor scheduling, tags, fix html bug + +changeset: 189:cf9a933c6449 +user: Damien Elmes +date: Fri Jul 20 13:58:19 2007 +0900 +description: +card models prevent the same fact from being seen in succession, fix stats + +changeset: 188:e3010a8da9be +user: Damien Elmes +date: Fri Jul 20 12:49:59 2007 +0900 +description: +add priority definitions, support left/right alignment + +changeset: 187:9fe0ef29addd +user: Damien Elmes +date: Thu Jul 19 04:07:42 2007 +0900 +description: +start work on syncing, remove some obsolete files, ensure identical modtime + +changeset: 186:d89c9c919ccb +user: Damien Elmes +date: Thu Jul 19 00:28:55 2007 +0900 +description: +new, more robust saving/backup code + +changeset: 185:8862d5c849ed +user: Damien Elmes +date: Wed Jul 18 23:12:38 2007 +0900 +description: +improve modification handling, more refactoring + +changeset: 184:f1244e6be152 +user: Damien Elmes +date: Wed Jul 18 19:14:28 2007 +0900 +description: +refactor: fields in fact as dict, modified notify parent, more models + +changeset: 183:9c6cca3a4fd2 +user: Damien Elmes +date: Tue Jul 17 18:36:27 2007 +0900 +description: +another attempt at an old fc bug + +changeset: 182:e746c76030dc +user: Damien Elmes +date: Tue Jul 17 00:04:23 2007 +0900 +description: +set mac font, catch hiragana = () on import + +changeset: 181:1d6ebb352b9c +user: Damien Elmes +date: Sun Jul 15 04:41:26 2007 +0900 +description: +default to platform specific font + +changeset: 180:81c65df62634 +user: Damien Elmes +date: Sun Jul 15 04:03:43 2007 +0900 +description: +allow card models to define the question in the answer + +changeset: 179:21fe78af3d93 +user: Damien Elmes +date: Sun Jul 15 03:33:27 2007 +0900 +description: +refactor getStats(), update card stats report + +changeset: 178:4c1637edc64b +user: Damien Elmes +date: Sun Jul 15 02:55:29 2007 +0900 +description: +record history when answering cards + +changeset: 177:655a417fda8d +user: Damien Elmes +date: Sun Jul 15 02:25:55 2007 +0900 +description: +update stats: track per day, and per card + +changeset: 176:0353df9f0e24 +user: Damien Elmes +date: Sat Jul 14 15:55:44 2007 +0900 +description: +make show all readings when something's ambiguous + +changeset: 175:04e88ddf54fe +user: Damien Elmes +date: Sat Jul 14 14:03:02 2007 +0900 +description: +fix bug in sched refactor, bug in kanji check if there's a newline + +changeset: 174:9db4df9713eb +user: Damien Elmes +date: Sat Jul 14 13:38:38 2007 +0900 +description: +remove todo, the bug tracker covers it better + +changeset: 173:e0c55744c437 +user: Damien Elmes +date: Sat Jul 14 13:35:11 2007 +0900 +description: +revert to gpl 2+, as we're waiting on qt + +changeset: 172:a468511a39dc +user: Damien Elmes +date: Sat Jul 14 13:31:39 2007 +0900 +description: +shuffle around some code in sched, remove the scheduling exception class + +changeset: 171:5153f1f1952c +user: Damien Elmes +date: Fri Jul 13 17:15:23 2007 +0900 +description: +unify fact errors, refactor validation + +changeset: 170:8540466d93bc +user: Damien Elmes +date: Thu Jul 12 03:10:13 2007 +0900 +description: +add routine to fetch all tags in use, change ensureUnique, add isInvalid + +changeset: 169:162c126fdd69 +user: Damien Elmes +date: Wed Jul 11 22:20:59 2007 +0900 +description: +remove facts when all associated cards have been deleted + +changeset: 168:04b71b867c20 +user: Damien Elmes +date: Wed Jul 11 11:52:59 2007 +0900 +description: +"" shouldn't match non-existent fields + +changeset: 167:eec8f66543b8 +user: Damien Elmes +date: Wed Jul 11 11:41:50 2007 +0900 +description: +properly report empty fields even in html mode + +changeset: 166:0ab5d3bfb46f +user: Damien Elmes +date: Wed Jul 11 11:31:11 2007 +0900 +description: +don't add identical reading even if there's a newline in expression + +changeset: 165:1ee5e5f42480 +user: Damien Elmes +date: Tue Jul 10 23:24:46 2007 +0900 +description: +fix kakasi bug, integrate properly, make both front and back unique in simple + +changeset: 164:7c216a5290b6 +user: Damien Elmes +date: Tue Jul 10 16:40:10 2007 +0900 +description: +default to arial/20 for card editing + +changeset: 163:aa3135d44e87 +user: Damien Elmes +date: Mon Jul 09 23:16:27 2007 +0900 +description: +fix a bug in importing code if a unique field is not mapped + +changeset: 162:8172d937f3b0 +user: Damien Elmes +date: Sat Jul 07 17:27:40 2007 +0900 +description: +html+text q/a, uppercase some tags, update colours in models, use css, add .ru + +changeset: 161:4720d8b2e383 +user: Damien Elmes +date: Thu Jul 05 02:08:15 2007 +0900 +description: +fix two graph bugs, change importing list, tests + +changeset: 160:fb410454176e +user: Damien Elmes +date: Thu Jul 05 00:07:16 2007 +0900 +description: +use html to separate fields by default + +changeset: 159:d3ec18287695 +user: Damien Elmes +date: Mon Jul 02 19:17:11 2007 +0900 +description: +fix kakasi bug, update stats/graphs, work on decorators, add deck properties, more + +changeset: 158:d39f70c24eda +user: Damien Elmes +date: Sun Jul 01 05:56:58 2007 +0900 +description: +start work on decorators, tidy up models + +changeset: 157:7cbfe8deb03d +user: Damien Elmes +date: Sun Jul 01 04:33:51 2007 +0900 +description: +ensure a current model on deletion, fix bug, make delays customizable + +changeset: 156:d214d7568bda +user: Damien Elmes +date: Sun Jul 01 02:41:18 2007 +0900 +description: +routines to fetch models/fields by name, improve importing, more + +changeset: 155:541a83f4408d +user: Damien Elmes +date: Sat Jun 30 06:03:26 2007 +0900 +description: +add support for importing old fc .pending files, fix text import + +changeset: 154:35177c144190 +user: Damien Elmes +date: Sat Jun 30 05:18:17 2007 +0900 +description: +move to gpl3, update headers and COPYING + +changeset: 153:57ee89add903 +user: Damien Elmes +date: Sat Jun 30 05:13:35 2007 +0900 +description: +mostly finished importing + +changeset: 152:2271bf6557de +user: Damien Elmes +date: Thu Jun 28 04:43:29 2007 +0900 +description: +model & importing changes +- models now just use a single class +- cards can be disabled and enabled using cards[] from allcards[] +- improve ankiv2 importing, start work on importing classes + +changeset: 151:1bf90a06af5e +user: Damien Elmes +date: Mon Jun 25 02:28:05 2007 +0900 +description: +fields should use 'description', not 'info' + +changeset: 150:e2c22d2b51fa +user: Damien Elmes +date: Sun Jun 24 03:48:56 2007 +0900 +description: +add tag parsing + +changeset: 149:ea1f1a805bd9 +user: Damien Elmes +date: Sun Jun 24 03:40:24 2007 +0900 +description: +allow ease 2 other than 1 day + +changeset: 148:9657b0443164 +user: Damien Elmes +date: Sun Jun 24 01:22:12 2007 +0900 +description: +add deck name/description/syncing target + +changeset: 147:cc473a00f280 +user: Damien Elmes +date: Sat Jun 23 02:40:34 2007 +0900 +description: +importing old anki decks mostly working + +changeset: 146:900d9b946e12 +user: Damien Elmes +date: Thu Jun 21 05:45:19 2007 +0900 +description: +manager->scheduler, refactor parts of deck, more tests + +changeset: 145:8f5bf703cb7d +user: Damien Elmes +date: Thu Jun 21 03:30:47 2007 +0900 +description: +more work on facts/errors, change __init__.py, start on deck + +changeset: 144:b55b9283b1d4 +user: Damien Elmes +date: Thu Jun 21 01:34:56 2007 +0900 +description: +i18n with gettext, refactor utils, remove egg info + +changeset: 143:82249912b66e +user: Damien Elmes +date: Wed Jun 20 23:52:31 2007 +0900 +description: +retire sm5.py and fc compat code + +changeset: 142:0e93e02d9092 +user: Damien Elmes +date: Wed Jun 20 23:50:27 2007 +0900 +description: +pluralise modules, add more tests, fact code + +changeset: 141:54f0a36b3348 +user: Damien Elmes +date: Tue Jun 19 18:44:40 2007 +0900 +description: +strip old sync code + +changeset: 140:5629a1e8bc12 +user: Damien Elmes +date: Tue Jun 19 18:44:09 2007 +0900 +description: +start changes to deck.py, rename lastModified to modified + +changeset: 139:d720504e31c0 +user: Damien Elmes +date: Tue Jun 19 04:10:41 2007 +0900 +description: +more testcases, bugfixes + +changeset: 138:857a88ac45c4 +user: Damien Elmes +date: Tue Jun 19 02:09:34 2007 +0900 +description: +very beginnings of new card/deck model + +changeset: 137:aa5a3268b901 +user: Damien Elmes +date: Sat Jun 16 22:34:40 2007 +0900 +description: +add test case for syncing bug + +changeset: 136:092477f5243e +user: Damien Elmes +date: Tue Jun 12 12:24:44 2007 +0900 +description: +fix references to errors that were broken in the refactor + +changeset: 135:37c672ef614f +user: Damien Elmes +date: Sat Jun 09 19:50:26 2007 +0900 +description: +add cumulative due graph, remove guide bars at 180+ days + +changeset: 134:71ef9355c144 +user: Damien Elmes +date: Sat Jun 09 19:23:53 2007 +0900 +description: +base graphs on the start of the day, not the current time + +changeset: 133:7a1269ff66c8 +user: Damien Elmes +date: Sat Jun 09 18:27:36 2007 +0900 +description: +add tests for special chars + +changeset: 132:95b88334e11f +user: Damien Elmes +date: Sat Jun 09 18:24:34 2007 +0900 +description: +allow :: in card definitions, as it's no longer required by the sync protocol + +changeset: 131:1131f08a06b3 +user: Damien Elmes +date: Sat Jun 09 18:22:36 2007 +0900 +description: +upgrade sync protocol, rename controller methods, more +- use json instead of building our own protocol +- define the server logic in sync.py instead of in the web code +- include json2.py, which is a modified version of json.py that supports utf-8 +- Controller.newDeck() -> Controller.Deck(), etc +- add unit tests for syncing +- change card repr format + +changeset: 130:359982e7b9a5 +user: Damien Elmes +date: Sat Jun 09 04:58:38 2007 +0900 +description: +add mnemosyne import support + +changeset: 129:bad5bb9fd2a2 +user: Damien Elmes +date: Sat Jun 09 03:45:30 2007 +0900 +description: +allow control over reverse gen/randomizing when importing text + +changeset: 128:da56fc31d7b0 +user: Damien Elmes +date: Sat Jun 09 02:55:32 2007 +0900 +description: +allow choice of append/random in text import + +changeset: 127:5ec4ed09cbce +user: Damien Elmes +date: Thu Jun 07 02:00:28 2007 +0900 +description: +remove timeout (syncing is threaded now, and it fixes a osx bug) + +changeset: 126:1526f7369672 +user: Damien Elmes +date: Wed Jun 06 15:31:22 2007 +0900 +description: +fix 3+4 deck (had furigana in questions) + +changeset: 125:6f7da75f7f6a +user: Damien Elmes +date: Sat Jun 02 08:44:30 2007 +0900 +description: +fix firstAnswered syncing problem + +changeset: 124:5a7e5bed1787 +user: Damien Elmes +date: Fri Jun 01 23:42:33 2007 +0900 +description: +replace unicode ~ with ascii + +changeset: 123:f9368a648531 +user: Damien Elmes +date: Fri Jun 01 22:44:45 2007 +0900 +description: +append cards when importing - don't shuffle + +changeset: 122:024e589e22f8 +user: Damien Elmes +date: Thu May 24 00:57:02 2007 +0900 +description: +fix bug with duplicates caused by the same kanji used twice in a card + +changeset: 121:8c6df176b34d +user: Damien Elmes +date: Thu May 24 00:10:24 2007 +0900 +description: +add a grid + +changeset: 120:e7133425e4fd +user: Damien Elmes +date: Wed May 23 23:58:52 2007 +0900 +description: +add eases graph + +changeset: 119:88d3066be6c5 +user: Damien Elmes +date: Tue May 22 03:24:15 2007 +0900 +description: +implement iroiro's kanji stats + +changeset: 118:0b65cc61335e +user: Damien Elmes +date: Mon May 21 10:02:28 2007 +0900 +description: +make that minutes, not days + +changeset: 117:17dfdf49e740 +user: Damien Elmes +date: Mon May 21 10:01:02 2007 +0900 +description: +set last interval = 10 minutes when card is wrong + +changeset: 116:192e631ac341 +user: Damien Elmes +date: Mon May 21 06:05:20 2007 +0900 +description: +fix bug w/ firstAnswered + syncing + +changeset: 115:ac6ece2f2228 +user: Damien Elmes +date: Sun May 20 12:48:19 2007 +0900 +description: +reduce ease 1 by double the reciprocal + +changeset: 114:3862661c41ab +user: Damien Elmes +date: Sun May 20 01:14:48 2007 +0900 +description: +check for existing firstAnswered (due to sync, etc) + +changeset: 113:b924786ffd1a +user: Damien Elmes +date: Sun May 20 01:07:04 2007 +0900 +description: +fix bug with firstAnswered + +changeset: 112:608d244c0e90 +user: Damien Elmes +date: Sun May 20 00:47:56 2007 +0900 +description: +easy interval to 7-9 + +changeset: 111:d4b3f0b60f26 +user: Damien Elmes +date: Sun May 20 00:47:14 2007 +0900 +description: +mid interval to 3-5 + +changeset: 110:287513b6da37 +user: Damien Elmes +date: Sat May 19 23:49:24 2007 +0900 +description: +remove rogue print statement + +changeset: 109:694b1710e3b2 +user: Damien Elmes +date: Sat May 19 22:04:13 2007 +0900 +description: +enforce ylim difference of 1 + +changeset: 108:f899bc0a15ca +user: Damien Elmes +date: Sat May 19 21:46:50 2007 +0900 +description: +fix bug in upgrading decks + +changeset: 107:08490dc2fde3 +user: Damien Elmes +date: Sat May 19 21:02:42 2007 +0900 +description: +1 hours -> 1 hour + +changeset: 106:a9c25fdfe93e +user: Damien Elmes +date: Sat May 19 20:55:46 2007 +0900 +description: +add firstAnswered attribute + +changeset: 105:7da604081893 +user: Damien Elmes +date: Fri May 18 21:30:34 2007 +0900 +description: +apply only half delay to ease 3 when card is new + +changeset: 104:1da0cdc058c7 +user: Damien Elmes +date: Fri May 18 20:53:24 2007 +0900 +description: +display a date range for cards in the initial state + +changeset: 103:6fb3acd2381b +user: Damien Elmes +date: Thu May 17 03:45:51 2007 +0900 +description: +don't apply delay factor to new cards in nextTimeStr() + +changeset: 102:f8e7de875cf6 +user: Damien Elmes +date: Wed May 16 23:03:24 2007 +0900 +description: +remove redundant 'jouyou' (fixes display on osx) + +changeset: 101:57bb6a5ac01b +user: Damien Elmes +date: Wed May 16 22:47:08 2007 +0900 +description: +don't import pylab + +changeset: 100:8b4b7d3ca8aa +user: Damien Elmes +date: Wed May 16 04:03:38 2007 +0900 +description: +never apply a negative delay +(this is not a problem usually - only useful in testing) + +changeset: 99:94484f221a49 +user: Damien Elmes +date: Wed May 16 04:02:06 2007 +0900 +description: +don't depend on card having a deviation attribute + +changeset: 98:1cfe2b1f9dfb +user: Damien Elmes +date: Tue May 15 04:42:55 2007 +0900 +description: +fix call to fmtTimeSpan + +changeset: 97:17c426fbc2fe +user: Damien Elmes +date: Tue May 15 03:50:07 2007 +0900 +description: +don't show the last interval if it hasn't been updated yet + +changeset: 96:954a3a150ec1 +user: Damien Elmes +date: Tue May 15 02:40:47 2007 +0900 +description: +make importing code give more information on failures + +changeset: 95:275e78c98747 +user: Damien Elmes +date: Tue May 15 01:36:47 2007 +0900 +description: +conditionally import pkg_resources +this allows anki to load even if setuptools is not available. + +changeset: 94:80eff21fadc1 +user: Damien Elmes +date: Tue May 15 00:42:53 2007 +0900 +description: +days with 0 in the graphs should plot as 0 + +changeset: 93:fae9e7f25e5f +user: Damien Elmes +date: Tue May 15 00:29:24 2007 +0900 +description: +add missing lang.py, disable bars at 1-5 years + +changeset: 92:e4b8b9e57e7d +user: Damien Elmes +date: Mon May 14 23:42:22 2007 +0900 +description: +fix problem with setLang() due to refactor + +changeset: 91:a13cdb8a6f7c +user: Damien Elmes +date: Mon May 14 10:47:06 2007 +0900 +description: +by default, don't pad time strings + +changeset: 90:4bbe3d4d2a20 +user: Damien Elmes +date: Mon May 14 10:39:32 2007 +0900 +description: +only use delay factor if ease > 2 + +changeset: 89:ad840a50cfc4 +user: Damien Elmes +date: Mon May 14 00:43:35 2007 +0900 +description: +show young/initial state in card stats + +changeset: 88:031312e80d70 +parent: 86:4673020de790 +parent: 87:e01d0c85749e +user: Damien Elmes +date: Sun May 13 23:28:02 2007 +0900 +description: +merge with other computer + +changeset: 87:e01d0c85749e +parent: 78:ba2db11330fc +user: Damien Elmes +date: Sun May 13 21:50:57 2007 +0900 +description: +don't check for same answer, only question + +changeset: 86:4673020de790 +user: Damien Elmes +date: Sun May 13 23:04:10 2007 +0900 +description: +finish dds's refactoring + +changeset: 85:6dd7d05c99a0 +parent: 78:ba2db11330fc +parent: 84:8ca19a3caba7 +user: Damien Elmes +date: Sun May 13 20:51:14 2007 +0900 +description: +merge changes from dds + +changeset: 84:8ca19a3caba7 +user: David Smith +date: Sun May 06 03:26:17 2007 +0900 +description: +Update file formatting + +changeset: 83:c3d540d7b881 +user: David Smith +date: Sun May 06 03:08:24 2007 +0900 +description: +Update file formatting + +changeset: 82:c562ade159c0 +user: David Smith +date: Sun May 06 02:21:01 2007 +0900 +description: +Initial refactoring away from putting everything in __init__.py + +changeset: 81:adf8acf41015 +user: David Smith +date: Sun May 06 02:20:34 2007 +0900 +description: +Move samples into anki directory so they can be handled as data + +changeset: 80:f5a0c4d03d62 +user: David Smith +date: Sun May 06 02:19:59 2007 +0900 +description: +Include anki egg-info + +changeset: 79:5ef15f6eebc4 +parent: 65:2052d232cd13 +user: David Smith +date: Sun May 06 02:19:44 2007 +0900 +description: +Use anki package name instead of libanki + +changeset: 78:ba2db11330fc +user: Damien Elmes +date: Sun May 13 05:44:22 2007 +0900 +description: +when adding a card, set lastInterval to startInterval + +changeset: 77:8aa4903582eb +user: Damien Elmes +date: Sun May 13 05:08:24 2007 +0900 +description: +add card stats + +changeset: 76:7d44927efc93 +user: Damien Elmes +date: Sun May 13 03:28:25 2007 +0900 +description: +add cardIsInInitialState(), use lastFactor in cardIsYoung if properly defined + +changeset: 75:56e9f0634f4d +user: Damien Elmes +date: Sun May 13 03:04:23 2007 +0900 +description: +bugfixes re young cards, lastInterval +- cardIsYoung() now is true if the current interval < easyInterval[0], which +means that the retention ratio may drop a little, but the terminolgy is +clearer now. perhaps in the future we can use a different category other than +"young". +- fix problems with lastInterval - wasn't being set for new cards, was being +set to a string when syncing + +changeset: 74:7e5c97355236 +user: Damien Elmes +date: Sun May 13 02:24:47 2007 +0900 +description: +hack for cardIsYoung() (will use lastInterval instead in the future) + +changeset: 73:6f574edc6013 +user: Damien Elmes +date: Sun May 13 02:18:57 2007 +0900 +description: +spread ease3/4 young cards out over 3 days + +changeset: 72:23f550604fa4 +user: Damien Elmes +date: Sun May 13 01:38:07 2007 +0900 +description: +check for lastCardRetrieved conditionally + +changeset: 71:53b34cc2669e +user: Damien Elmes +date: Fri May 11 01:39:31 2007 +0900 +description: +don't pass deviation to scheduleCard() + +changeset: 70:9647fc661acf +user: Damien Elmes +date: Fri May 11 01:37:29 2007 +0900 +description: +store deviation in card instead of making calling library deal with it + +changeset: 69:d3f2c758ac29 +user: Damien Elmes +date: Fri May 11 00:33:01 2007 +0900 +description: +add generator and getCards() for fetching multiple pending cards + +changeset: 68:f2226684c7d2 +user: Damien Elmes +date: Mon May 07 17:10:09 2007 +0900 +description: +update factor after rescheduling, allow deviation in nextIntervalStr + +changeset: 67:1a44539c2b5d +user: Damien Elmes +date: Mon May 07 16:51:59 2007 +0900 +description: +allow the calling library to pass in the deviation + +changeset: 66:2005d36d9dc5 +user: Damien Elmes +date: Sun May 06 23:42:39 2007 +0900 +description: +when syncing, reset only the pending info, not session performance + +changeset: 65:2052d232cd13 +user: Damien Elmes +date: Fri May 04 05:29:03 2007 +0900 +description: +don't update modified time on cards when upgrading + +changeset: 64:6e48ce323968 +user: Damien Elmes +date: Fri May 04 02:56:12 2007 +0900 +description: +update emacs version for latest stats + +changeset: 63:76b4db221b22 +user: Damien Elmes +date: Fri May 04 02:50:39 2007 +0900 +description: +pave way for checking lastInterval instead of interval to determine young +cards (upgrade deck version) + +changeset: 62:c0bb2e142bb3 +user: Damien Elmes +date: Fri May 04 02:31:14 2007 +0900 +description: +make stats more accurately report 1/4/8 + +changeset: 61:572de05ef44c +user: Damien Elmes +date: Fri May 04 02:29:57 2007 +0900 +description: +cards less than 16 days are treated as young + +changeset: 60:c5040f15f846 +user: Damien Elmes +date: Thu May 03 10:27:48 2007 +0900 +description: +fix typo in jlpt 3 and 4 + +changeset: 59:d96c9e914cc4 +user: Damien Elmes +date: Wed May 02 21:36:09 2007 +0900 +description: +refactor interval stats + +changeset: 58:2a0b417ae524 +user: Damien Elmes +date: Wed May 02 20:23:59 2007 +0900 +description: +revert to indiscriminate reduction of pending cards again + +changeset: 57:8c4aa3c5b6d6 +user: Damien Elmes +date: Wed May 02 19:28:13 2007 +0900 +description: +fix missing furigana in jlpt3 deck + +changeset: 56:d715861a6e6d +user: Damien Elmes +date: Wed May 02 18:41:01 2007 +0900 +description: +bump up libanki version number + +changeset: 55:5b5dce44a670 +user: Damien Elmes +date: Wed May 02 18:36:42 2007 +0900 +description: +remove upgrading message + +changeset: 54:ae135de7aa0d +user: Damien Elmes +date: Wed May 02 18:33:50 2007 +0900 +description: +more stat fixes, add 'cards added' graph + +changeset: 53:a80da6c747ca +user: Damien Elmes +date: Wed May 02 18:16:17 2007 +0900 +description: +a few tweaks to stats output, change graph colours + +changeset: 52:28348c6f854f +user: Damien Elmes +date: Wed May 02 18:03:52 2007 +0900 +description: +a number of scheduling and stats changes +- add cardIsYoung() and make young cards more explicit +- don't deviate young cards, the interval is too small. +- ease 2 on mature cards repeats same time as last time +- ease 2 doesn't change card factor +- ease 1 reduces factor by 1 step, not 2 +- don't discriminately reduce pending for incorrect cards + (reverses previous decision - needs more work to be clear + to the user) +- improve getStats() - return comprehensive short names for + both session and global statistics +- store correct/incorrect for global and session, divided + into three categories: new, young and mature cards +- questions taking more than 60 seconds to answer don't + change the estimated time (the user probably walked away) +- divide ease stats into new, young, old too +- upgrade the deck to version 2, necessary for stats changes. + any deck retention numbers will be lost as they're not reliable. +- update statistics to take advantage of the new categories (missing ease + stats for now - work in progress) + +changeset: 51:37bf2c17387f +user: Damien Elmes +date: Wed May 02 06:17:38 2007 +0900 +description: +don't use assert to run a command, it gets optimised away. +need to improve that handling more. + +changeset: 50:87280bd54075 +user: Damien Elmes +date: Wed May 02 02:57:50 2007 +0900 +description: +export global answered/correct & pending old/new in stats + +changeset: 49:8749f1646de3 +user: Damien Elmes +date: Mon Apr 30 23:01:53 2007 +0900 +description: +apply half the delay factor to new cards at ease 3 + +changeset: 48:8ca677ddcc28 +user: Damien Elmes +date: Mon Apr 30 22:54:53 2007 +0900 +description: +show total cards in stats + +changeset: 47:8cf0d0d2971f +user: Damien Elmes +date: Mon Apr 30 21:58:12 2007 +0900 +description: +make sure the time module is available + +changeset: 46:fe5ef6663b13 +user: Damien Elmes +date: Mon Apr 30 20:48:02 2007 +0900 +description: +more deck stat format changes + +changeset: 45:90275bb7766b +user: Damien Elmes +date: Mon Apr 30 20:40:56 2007 +0900 +description: +more stats, reset creation time when importing cards + +changeset: 44:e89d2153e94f +user: Damien Elmes +date: Sun Apr 29 17:40:38 2007 +0900 +description: +fix an error in furigana for 'karai' in the sample decks + +changeset: 43:d6e0269febbd +user: Damien Elmes +date: Sun Apr 29 04:19:26 2007 +0900 +description: +capitalize jinmeiyou in kanjistats + +changeset: 42:096652813b9b +user: Damien Elmes +date: Sat Apr 28 00:43:35 2007 +0900 +description: +add x axis label in graphs + +changeset: 41:1870732f2c64 +user: Damien Elmes +date: Fri Apr 27 01:11:29 2007 +0900 +description: +fix definition in jlpt2 deck + +changeset: 40:4261fdfb6e98 +user: Damien Elmes +date: Fri Apr 27 01:09:37 2007 +0900 +description: +append a final newline when exporting text, add ExportFileError exception + +changeset: 39:9095ae2ef085 +user: Damien Elmes +date: Thu Apr 26 04:44:58 2007 +0900 +description: +improve sample files, add export support, fix graphs +- add jlpt 2-4 exerpts derived from my own decks +- add a tool to derive jlpt vocab from a jlpt deck and personal deck +- add export to anki (clean) deck, export to text +- render fill graphs down to the bottom of the graph +- don't allow graphs with insufficient info to be generated + +changeset: 38:906efae0a582 +user: Damien Elmes +date: Wed Apr 25 09:13:52 2007 +0900 +description: +add missing 'card is new' check in scheduling + +changeset: 37:6b859b82f7da +user: Damien Elmes +date: Wed Apr 25 06:42:27 2007 +0900 +description: +add graphs file + +changeset: 36:bae57f08f58d +user: Damien Elmes +date: Wed Apr 25 06:12:22 2007 +0900 +description: +set matlab path for frozen, factor into separate file for lazy load + +changeset: 35:5a220e427dd0 +user: Damien Elmes +date: Wed Apr 25 04:43:52 2007 +0900 +description: +apply delay to non-new cards in initial state, too + +changeset: 34:f7f7005ef9fc +user: Damien Elmes +date: Wed Apr 25 04:33:36 2007 +0900 +description: +use anki's version number in setup.py + +changeset: 33:3aa6712b8615 +user: Damien Elmes +date: Wed Apr 25 03:58:13 2007 +0900 +description: +add graph generation + +changeset: 32:96017e5c4f27 +user: Damien Elmes +date: Wed Apr 25 03:03:46 2007 +0900 +description: +don't deviate initial ease 4 negatively + +changeset: 31:b1d9ae79b167 +user: David Smith +date: Mon Apr 23 14:44:55 2007 +0900 +description: +Include forgotten files + +changeset: 30:92f914e17ae2 +user: David Smith +date: Mon Apr 23 02:08:33 2007 +0900 +description: +Make the utils into a setup-tools command entrypoint + +changeset: 29:1b3d0de96681 +user: David Smith +date: Mon Apr 23 02:08:10 2007 +0900 +description: +Add setup.py + +changeset: 28:7174f94a73ed +user: Damien Elmes +date: Sun Apr 22 03:26:37 2007 +0900 +description: +add exception for non-utf8 imports + +changeset: 27:d565a91a0159 +user: Damien Elmes +date: Sun Apr 22 01:52:45 2007 +0900 +description: +add support for importing anki decks and text files + +changeset: 26:da32a4e405e3 +user: Damien Elmes +date: Fri Apr 20 17:16:12 2007 +0900 +description: +don't factor in delay on new cards + +changeset: 25:b24d8fc1f3ce +user: Damien Elmes +date: Fri Apr 20 15:58:36 2007 +0900 +description: +properly report an empty response from the server when syncing + +changeset: 24:32771814f083 +user: Damien Elmes +date: Fri Apr 20 06:55:20 2007 +0900 +description: +add missing kanji stats + +changeset: 23:72e015d83b4a +user: Damien Elmes +date: Wed Apr 18 19:21:16 2007 +0900 +description: +fix pending cards bug, integrate lac's kanji stats + +changeset: 22:a2fea56e5482 +user: Damien Elmes +date: Wed Apr 18 07:46:42 2007 +0900 +description: +update stats after card change (fixes pending: 1 bug) + +changeset: 21:43918a2d67ba +user: Damien Elmes +date: Wed Apr 18 04:30:09 2007 +0900 +description: +cards answered latest than scheduled are scheduled further in the future + +changeset: 20:8ae96c675b7d +user: Damien Elmes +date: Tue Apr 17 12:32:31 2007 +0900 +description: +add a string report for kanji + +changeset: 19:931c573b3788 +user: Damien Elmes +date: Mon Apr 16 23:57:51 2007 +0900 +description: +fix bug in kanjistats + +changeset: 18:5fbed8c50d94 +user: Damien Elmes +date: Mon Apr 16 23:38:07 2007 +0900 +description: +tweaks to stats html + +changeset: 17:f706357905ac +user: Damien Elmes +date: Mon Apr 16 23:26:33 2007 +0900 +description: +add deck stats (from ankiqt), return matching card in hasQuestion() + +changeset: 16:5aa76d905ea2 +user: Damien Elmes +date: Wed Apr 11 17:39:24 2007 +0900 +description: +refactor pending cards into new/old, other changes +- ensure old cards are shown first, even if new cards were randomly placed +earlier +- separate pending into pendingOld and pendingNew +- remove formatTimeDiff from the scheduling code and place it in __init__.py +- reduce pendingOld even if a card was wrong + +changeset: 15:2f24abf50f2d +user: Damien Elmes +date: Mon Apr 09 15:48:10 2007 +0900 +description: +bump factorFour up, create easeStats on new deck, show time in seconds too + +changeset: 14:ce95db97d442 +user: Damien Elmes +date: Sat Apr 07 17:12:28 2007 +0900 +description: +track each ease answer the user gives (could be useful in the future) + +changeset: 13:deb2889ad180 +user: Damien Elmes +date: Sat Apr 07 17:07:05 2007 +0900 +description: +display modified cards first, fix bug in addCard, send client version +- previous seen cards are shown before new ones +- fixed an infinite loop when adding a card with the same question as a +previously renamed card +- send the protocol & client version when syncing + +changeset: 12:79c9239d1c8f +user: Damien Elmes +date: Thu Apr 05 02:47:22 2007 +0900 +description: +import sm5 to ensure it's included in the windows exe + +changeset: 11:b70e087578f5 +user: Damien Elmes +date: Wed Apr 04 22:53:23 2007 +0900 +description: +track estimated time to deck finish + +changeset: 10:01e18aedb3f9 +user: Damien Elmes +date: Wed Apr 04 19:46:56 2007 +0900 +description: +deviate interval when card is in initial state, too + +changeset: 9:9717eeae72c6 +user: Damien Elmes +date: Wed Apr 04 19:41:59 2007 +0900 +description: +more scheduling updates +- newly added cards now give you a choice of 1, 4 and 8 days +- until past the first 8 days, the card will be treated as new +- give easy cards an extra 1.2x boost (5 years in 8-9 iters) +- make ease=3 factor a little higher, increase factor each time +- limit card scheduling to 5 years by default +- add a 'years' output for when cards reach over a year + +changeset: 8:9337a39a4460 +user: Damien Elmes +date: Tue Apr 03 18:03:26 2007 +0900 +description: +handle old sm5 decks too when renaming to anki + +changeset: 7:5f443860ed27 +user: Damien Elmes +date: Tue Apr 03 04:26:20 2007 +0900 +description: +rename fc to anki (pt 2) + +changeset: 6:aa0f404497c1 +user: Damien Elmes +date: Tue Apr 03 03:48:52 2007 +0900 +description: +rename fc to anki + +changeset: 5:beda7105baea +user: Damien Elmes +date: Sat Mar 31 16:25:01 2007 +0900 +description: +remove decimal point from scheduling output (apparently it's confusing) + +changeset: 4:32077af83c04 +user: Damien Elmes +date: Sat Mar 31 16:18:22 2007 +0900 +description: +make ease=3 exactly between 2 & 4 + +changeset: 3:c876b1f3427e +user: Damien Elmes +date: Tue Mar 27 14:04:22 2007 +0900 +description: +update emacs interface to work with sched, add brief docs + +changeset: 2:3c53e48d26bd +user: Damien Elmes +date: Sun Mar 25 13:59:45 2007 -0700 +description: +if syncing causes local deck changes, mark the deck modified + +changeset: 1:7f2ce5c599cd +user: Damien Elmes +date: Sun Mar 25 13:53:04 2007 -0700 +description: +add version number, strip more emacs import code, add warning about sm5 being obsolete + +changeset: 0:344b29e2e1e8 +user: Damien Elmes +date: Fri Mar 23 10:31:36 2007 -0700 +description: +import from bzr, see ChangeLog.old + +------------------------------------------------------------ +revno: 3 +committer: Damien Elmes +branch nick: fc +timestamp: Thu 2007-03-22 09:19:27 -0700 +message: + support appending to end of deck, add samples, temporary hack for saving +------------------------------------------------------------ +revno: 2 +committer: Damien Elmes +branch nick: fc +timestamp: Thu 2007-03-22 08:37:20 -0700 +message: + update sync & upgrade code, set deckVersion on creation + - syncing now sends a protocol and client version on checkAuth + - don't touch lastModified when converting to the fc scheduler + - when correcting for sm5 mischeduling, do so idempotently + - make sure to convert new fc card attributes to float/int +------------------------------------------------------------ +revno: 1 +committer: Damien Elmes +branch nick: fc +timestamp: Tue 2007-03-20 11:47:51 -0700 +message: + import from darcs (see ChangeLog.old for previous history) +------------------------------------------------------------ +Tue Mar 20 11:27:51 PDT 2007 fc@ichi2.net + * move to new scheduling algorithm, add lang updates, etc + + The new scheduling algorithm is more conservative than sm5, and fixes a number + of problems with cards being scheduled too far into the future. The factor + matrix has been removed in favour of per-card factor modification. Each card + starts at factor=1.5, with factorChange=0.05. Easy cards are increased by + factorChange, difficult cards are reduced by it. In the future factorChange + could be modified to speed up ease adjustments. + + The new algorithm has five ease levels when answering a card, instead of the + earlier 6. + + - track totalCount, correctCount and succesivelyCorrect in cards for future + statistics + - apply deviation just before scheduling, so we don't confuse the user + - add quasi-i18n support via setLang to set format of fc string output + - add deckFormat and deckVersion to ease future upgrades + - fc.sched is now the default algorithm and doesn't require makeDefault() + - remove emacs deck conversion support (it was a hack) + - move emacs support and utilities into separate directories + - add GPL boilerplates & COPYING + +Fri Mar 9 05:19:26 PST 2007 fc@ichi2.net + * add a command to reset pending cards without updating statistics + +Fri Feb 23 23:32:30 PST 2007 fc@ichi2.net + * correct previous matrixsize patch + +Thu Feb 22 23:17:45 PST 2007 fc@ichi2.net + * ensure count and ease are bounded to the matrix size + +Wed Jan 24 09:40:22 PST 2007 LaC + * LaC lazy launch of kakasi + - kakasi is launched only when first used + - simpler availability check + +Wed Jan 24 08:04:40 PST 2007 LaC + * LaC shared kakasi + - use a single instance of kakasi, instead of launching it anew on every + keystroke; this makes the "add cards" dialog more responsive, at least on + my system + +Mon Jan 8 19:20:16 PST 2007 fc@ichi2.net + * display approximate time to next appearance + +Sat Dec 23 08:31:18 PST 2006 fc@ichi2.net + * find kakasi on mac + +Tue Dec 12 00:53:13 PST 2006 fc@ichi2.net + * fix negative numbers in status report + +Tue Dec 12 00:51:09 PST 2006 fc@ichi2.net + * improve backup/support old style format/provide deckCon + +Thu Dec 7 06:33:23 PST 2006 fc@ichi2.net + * handle deleted-and-readded cards and empty furigana + +Thu Dec 7 06:08:31 PST 2006 fc@ichi2.net + * add backup support + +Thu Dec 7 02:13:06 PST 2006 fc@ichi2.net + * if furigana is explictly provided, don't try invoke kakasi. no kana + generation + +Thu Dec 7 01:19:53 PST 2006 fc@ichi2.net + * use the :80 url for syncing + +Wed Dec 6 21:29:18 PST 2006 fc@ichi2.net + * implement add cards + +Wed Nov 29 20:30:19 PST 2006 fc@ichi2.net + * emacs support, big refactor + +Fri Nov 17 06:42:21 PST 2006 fc@ichi2.net + * add more deck creation support, various other things + +Wed Nov 15 10:37:30 PST 2006 fc@ichi2.net + * sync support w/ server working + +Tue Nov 14 10:31:25 PST 2006 fc@ichi2.net + * add deck editing and more + +Mon Nov 13 08:52:05 PST 2006 fc@ichi2.net + * add server support, syncing, etc + +Fri Nov 10 09:10:28 PST 2006 fc@ichi2.net + * misc patches + +Mon Oct 23 04:01:20 PDT 2006 fc@ichi2.net + * new interface seems okay for now + +Mon Oct 23 00:55:37 PDT 2006 fc@ichi2.net + * add missing files + +Mon Oct 23 00:55:00 PDT 2006 fc@ichi2.net + * move out fc_web, reorganise structure + +Sat Oct 21 16:57:31 PDT 2006 fc@ichi2.net + * add all new files from 3am commit + +Sat Oct 21 03:36:03 PDT 2006 fc@ichi2.net + * 3:30am commit + +Fri Oct 20 21:08:19 PDT 2006 fc@ichi2.net + * add pylons dir + +Fri Oct 20 21:06:10 PDT 2006 fc@ichi2.net + * initial import diff --git a/anki/__init__.py b/anki/__init__.py new file mode 100644 index 000000000..389b06a15 --- /dev/null +++ b/anki/__init__.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Anki (libanki) +==================== + +Open a deck: + + deck = anki.DeckStorage.Deck(path) + +Get a card: + + card = deck.getCard() + if not card: + # deck is finished + +Show the card: + + print card.question, card.answer + +Answer the card: + + deck.answerCard(card, ease) + +Edit the card: + + fields = card.fact.model.fieldModels + for field in fields: + card.fact[field.name] = "newvalue" + card.fact.setModified(textChanged=True) + deck.setModified() + +Get all cards via ORM (slow): + + from anki.cards import Card + cards = deck.s.query(Card).all() + +Get all q/a/ids via SQL (fast): + + cards = deck.s.all("select id, question, answer from cards") + +Save & close: + + deck.save() + deck.close() +""" +__docformat__ = 'restructuredtext' + +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + pass + +version = "0.9.8.1" + +from anki.deck import DeckStorage diff --git a/anki/cards.py b/anki/cards.py new file mode 100644 index 000000000..3cf10cb2b --- /dev/null +++ b/anki/cards.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Cards +==================== +""" +__docformat__ = 'restructuredtext' + +import time, sys, math, random +from anki.db import * +from anki.models import CardModel, Model, FieldModel +from anki.facts import Fact, factsTable, Field +from anki.utils import parseTags, findTag, stripHTML, genID + +# Cards +########################################################################## + +cardsTable = Table( + 'cards', metadata, + Column('id', Integer, primary_key=True), + Column('factId', Integer, ForeignKey("facts.id"), nullable=False), + Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False), + Column('created', Float, nullable=False, default=time.time), + Column('modified', Float, nullable=False, default=time.time), + Column('tags', UnicodeText, nullable=False, default=u""), + Column('ordinal', Integer, nullable=False), + # cached - changed on fact update + Column('question', UnicodeText, nullable=False, default=u""), + Column('answer', UnicodeText, nullable=False, default=u""), + # default to 'normal' priority; + # this is indexed in deck.py as we need to create a reverse index + Column('priority', Integer, nullable=False, default=2), + Column('interval', Float, nullable=False, default=0), + Column('lastInterval', Float, nullable=False, default=0), + Column('due', Float, nullable=False, default=time.time), + Column('lastDue', Float, nullable=False, default=0), + Column('factor', Float, nullable=False, default=2.5), + Column('lastFactor', Float, nullable=False, default=2.5), + Column('firstAnswered', Float, nullable=False, default=0), + # stats + Column('reps', Integer, nullable=False, default=0), + Column('successive', Integer, nullable=False, default=0), + Column('averageTime', Float, nullable=False, default=0), + Column('reviewTime', Float, nullable=False, default=0), + Column('youngEase0', Integer, nullable=False, default=0), + Column('youngEase1', Integer, nullable=False, default=0), + Column('youngEase2', Integer, nullable=False, default=0), + Column('youngEase3', Integer, nullable=False, default=0), + Column('youngEase4', Integer, nullable=False, default=0), + Column('matureEase0', Integer, nullable=False, default=0), + Column('matureEase1', Integer, nullable=False, default=0), + Column('matureEase2', Integer, nullable=False, default=0), + Column('matureEase3', Integer, nullable=False, default=0), + Column('matureEase4', Integer, nullable=False, default=0), + # this duplicates the above data, because there's no way to map imported + # data to the above + Column('yesCount', Integer, nullable=False, default=0), + Column('noCount', Integer, nullable=False, default=0), + # cache + Column('spaceUntil', Float, nullable=False, default=0), + Column('relativeDelay', Float, nullable=False, default=0), + Column('isDue', Boolean, nullable=False, default=0), + Column('type', Integer, nullable=False, default=2), + Column('combinedDue', Integer, nullable=False, default=0)) + +class Card(object): + "A card." + + def __init__(self, fact=None, cardModel=None): + self.tags = u"" + self.id = genID() + # new cards start as new & due + self.type = 2 + self.isDue = True + self.timerStarted = False + self.timerStopped = False + if fact: + self.fact = fact + if cardModel: + self.cardModel = cardModel + self.ordinal = cardModel.ordinal + self.question = cardModel.renderQA(self, self.fact, "question") + self.answer = cardModel.renderQA(self, self.fact, "answer") + + htmlQuestion = property(lambda self: self.cardModel.renderQA( + self, self.fact, "question", format="html")) + htmlAnswer = property(lambda self: self.cardModel.renderQA( + self, self.fact, "answer", format="html")) + + def setModified(self): + self.modified = time.time() + + def startTimer(self): + self.timerStarted = time.time() + + def stopTimer(self): + self.timerStopped = time.time() + + def thinkingTime(self): + "Return the time this card's been shown." + return (self.timerStopped or time.time()) - self.timerStarted + + def css(self): + return self.cardModel.css() + self.fact.css() + + def genFuzz(self): + "Generate a random offset to spread intervals." + self.fuzz = random.uniform(0.95, 1.05) + + def updateStats(self, ease, state): + self.reps += 1 + if ease > 1: + self.successive += 1 + else: + self.successive = 0 + delay = self.thinkingTime() + # ignore any times over 60 seconds + if delay < 60: + self.reviewTime += delay + if self.averageTime: + self.averageTime = (self.averageTime + delay) / 2.0 + else: + self.averageTime = delay + # we don't track first answer for cards + if state == "new": + state = "young" + # update ease and yes/no count + attr = state + "Ease%d" % ease + setattr(self, attr, getattr(self, attr) + 1) + if ease < 2: + self.noCount += 1 + else: + self.yesCount += 1 + if not self.firstAnswered: + self.firstAnswered = time.time() + self.setModified() + + def hasTag(self, tag): + alltags = parseTags(self.tags + "," + + self.fact.tags + "," + + self.cardModel.name + "," + + self.fact.model.tags) + return findTag(tag, alltags) + + def fromDB(self, s, id): + r = s.first("select * from cards where id = :id", + id=id) + (self.id, + self.factId, + self.cardModelId, + self.created, + self.modified, + self.tags, + self.ordinal, + self.question, + self.answer, + self.priority, + self.interval, + self.lastInterval, + self.due, + self.lastDue, + self.factor, + self.lastFactor, + self.firstAnswered, + self.reps, + self.successive, + self.averageTime, + self.reviewTime, + self.youngEase0, + self.youngEase1, + self.youngEase2, + self.youngEase3, + self.youngEase4, + self.matureEase0, + self.matureEase1, + self.matureEase2, + self.matureEase3, + self.matureEase4, + self.yesCount, + self.noCount, + self.spaceUntil, + self.relativeDelay, + self.isDue, + self.type, + self.combinedDue) = r + + def toDB(self, s): + "Write card to DB. Note that isDue assumes card is not spaced." + if self.reps == 0: + self.type = 2 + elif self.successive: + self.type = 1 + else: + self.type = 0 + s.execute("""update cards set +modified=:modified, +tags=:tags, +interval=:interval, +lastInterval=:lastInterval, +due=:due, +lastDue=:lastDue, +factor=:factor, +lastFactor=:lastFactor, +firstAnswered=:firstAnswered, +reps=:reps, +successive=:successive, +averageTime=:averageTime, +reviewTime=:reviewTime, +youngEase0=:youngEase0, +youngEase1=:youngEase1, +youngEase2=:youngEase2, +youngEase3=:youngEase3, +youngEase4=:youngEase4, +matureEase0=:matureEase0, +matureEase1=:matureEase1, +matureEase2=:matureEase2, +matureEase3=:matureEase3, +matureEase4=:matureEase4, +yesCount=:yesCount, +noCount=:noCount, +spaceUntil = :spaceUntil, +relativeDelay = :interval / (strftime("%s", "now") - :due + 1), +isDue = :isDue, +type = :type, +combinedDue = max(:spaceUntil, :due) +where id=:id""", self.__dict__) + +mapper(Card, cardsTable, properties={ + 'cardModel': relation(CardModel), + 'fact': relation(Fact, backref="cards", primaryjoin= + cardsTable.c.factId == factsTable.c.id), + }) + +mapper(Fact, factsTable, properties={ + 'model': relation(Model), + 'fields': relation(Field, backref="fact", order_by=Field.c.ordinal), + 'lastCard': relation(Card, post_update=True, primaryjoin= + cardsTable.c.id == factsTable.c.lastCardId), + }) + +# Card deletions +########################################################################## + +cardsDeletedTable = Table( + 'cardsDeleted', metadata, + Column('cardId', Integer, ForeignKey("cards.id"), + nullable=False), + Column('deletedTime', Float, nullable=False)) diff --git a/anki/db.py b/anki/db.py new file mode 100644 index 000000000..d7f3374ef --- /dev/null +++ b/anki/db.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +DB tools +==================== + +SessionHelper is a wrapper for the standard sqlalchemy session, which provides +some convenience routines, and manages transactions itself. + +object_session() is a replacement for the standard object_session(), which +provides the features of SessionHelper, and avoids taking out another +transaction. +""" +__docformat__ = 'restructuredtext' + +try: + from pysqlite2 import dbapi2 as sqlite +except ImportError: + try: + from sqlite3 import dbapi2 as sqlite + except: + raise "Please install pysqlite2 or python2.5" +sqlite.enable_shared_cache(True) + +from sqlalchemy import (Table, Integer, Float, Column, MetaData, + ForeignKey, Boolean, String, Date, UniqueConstraint) +from sqlalchemy import create_engine +from sqlalchemy.orm import mapper, sessionmaker, relation, backref, \ + object_session as _object_session +from sqlalchemy.sql import select, text, and_ +from sqlalchemy.exceptions import DBAPIError, OperationalError + +# sqlalchemy didn't handle the move to unicodetext nicely +try: + from sqlalchemy import UnicodeText +except ImportError: + from sqlalchemy import Unicode + UnicodeText = Unicode + +metadata = MetaData() + +# this class assumes the provided session is called with transactional=False +class SessionHelper(object): + "Add some convenience routines to a session." + + def __init__(self, session, lock=False, transaction=True): + self._session = session + self._lock = lock + self._transaction = transaction + if self._transaction: + self._session.begin() + if self._lock: + self._lockDB() + self._seen = True + + def __getattr__(self, k): + return getattr(self.__dict__['_session'], k) + + def scalar(self, sql, **args): + return self.execute(text(sql), args).scalar() + + def all(self, sql, **args): + return self.execute(text(sql), args).fetchall() + + def first(self, sql, **args): + return self.execute(text(sql), args).fetchone() + + def column0(self, sql, **args): + return [x[0] for x in self.execute(text(sql), args).fetchall()] + + def statement(self, sql, **kwargs): + "Execute a statement without returning any results. Flush first." + self.execute(text(sql), kwargs) + + def statements(self, sql, data): + "Execute a statement across data. Flush first." + self.execute(text(sql), data) + + def __repr__(self): + return repr(self._session) + + def commit(self): + self._session.commit() + if self._transaction: + self._session.begin() + if self._lock: + self._lockDB() + + def _lockDB(self): + "Take out a write lock." + self._session.execute(text("update decks set modified=modified")) + +def object_session(*args): + s = _object_session(*args) + if s: + return SessionHelper(s, transaction=False) + return None + diff --git a/anki/deck.py b/anki/deck.py new file mode 100644 index 000000000..860a6ae90 --- /dev/null +++ b/anki/deck.py @@ -0,0 +1,1825 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +The Deck +==================== +""" +__docformat__ = 'restructuredtext' + +import tempfile, time, os, random, sys, re, stat, shutil, types +from heapq import heapify, heappush, heappop + +from anki.db import * +from anki.lang import _ +from anki.errors import DeckAccessError, DeckWrongFormatError +from anki.stdmodels import BasicModel +from anki.utils import parseTags, tidyHTML, genID, ids2str +from anki.history import CardHistoryEntry +from anki.models import Model, CardModel +from anki.stats import dailyStats, globalStats + +# ensure all the metadata in other files is loaded before proceeding +import anki.models, anki.facts, anki.cards, anki.stats +import anki.history, anki.media + +PRIORITY_HIGH = 4 +PRIORITY_MED = 3 +PRIORITY_NORM = 2 +PRIORITY_LOW = 1 +PRIORITY_NONE = 0 +MATURE_THRESHOLD = 21 +# need interval > 0 to ensure relative delay is ordered properly +NEW_INTERVAL = 0.001 +NEW_CARDS_LAST = 1 +NEW_CARDS_DISTRIBUTE = 0 + +# parts of the code assume we only have one deck +decksTable = Table( + 'decks', metadata, + Column('id', Integer, primary_key=True), + Column('created', Float, nullable=False, default=time.time), + Column('modified', Float, nullable=False, default=time.time), + Column('description', UnicodeText, nullable=False, default=u""), + Column('version', Integer, nullable=False, default=9), + Column('currentModelId', Integer, ForeignKey("models.id")), + # syncing + Column('syncName', UnicodeText), + Column('lastSync', Float, nullable=False, default=0), + # scheduling + ############## + # initial intervals + Column('hardIntervalMin', Float, nullable=False, default=0.333), + Column('hardIntervalMax', Float, nullable=False, default=0.5), + Column('midIntervalMin', Float, nullable=False, default=3.0), + Column('midIntervalMax', Float, nullable=False, default=5.0), + Column('easyIntervalMin', Float, nullable=False, default=7.0), + Column('easyIntervalMax', Float, nullable=False, default=9.0), + # delays on failure + Column('delay0', Integer, nullable=False, default=600), + Column('delay1', Integer, nullable=False, default=1200), + Column('delay2', Integer, nullable=False, default=28800), + # collapsing future cards + Column('collapseTime', Integer, nullable=False, default=1), + # priorities & postponing + Column('highPriority', UnicodeText, nullable=False, default=u"VeryHighPriority"), + Column('medPriority', UnicodeText, nullable=False, default=u"HighPriority"), + Column('lowPriority', UnicodeText, nullable=False, default=u"LowPriority"), + Column('suspended', UnicodeText, nullable=False, default=u"Suspended"), + # 0 is random, 1 is by input date + Column('newCardOrder', Integer, nullable=False, default=0), + # when to show new cards + Column('newCardSpacing', Integer, nullable=False, default=NEW_CARDS_DISTRIBUTE), + # limit the number of failed cards in play + Column('failedCardMax', Integer, nullable=False, default=20), + # number of new cards to show per day + Column('newCardsPerDay', Integer, nullable=False, default=20), + # currently unused + Column('sessionRepLimit', Integer, nullable=False, default=100), + Column('sessionTimeLimit', Integer, nullable=False, default=1800)) + +class Deck(object): + "Top-level object. Manages facts, cards and scheduling information." + + factorFour = 1.3 + initialFactor = 2.5 + maxScheduleTime = 1825 + + def __init__(self, path=None): + "Create a new deck." + # a limit of 1 deck in the table + self.id = 1 + # db session factory and instance + self.Session = None + self.s = None + + def _initVars(self): + self.lastTags = u"" + self.lastLoaded = time.time() + self._countsDirty = True + + def modifiedSinceSave(self): + return self.modified > self.lastLoaded + + # Getting the next card + ########################################################################## + + def getCard(self, orm=True): + "Return the next card object, or None." + id = self.getCardId() + if id: + return self.cardFromId(id, orm) + + def getCards(self, limit=1, orm=True): + """Return LIMIT number of new card objects. +Caller must ensure multiple cards of the same fact are not shown.""" + ids = self.getCardIds(limit) + return [self.cardFromId(x, orm) for x in ids] + + def getCardId(self): + "Return the next due card id, or None." + now = time.time() + ids = [] + # failed card due? + id = self.s.scalar("select id from failedCardsNow limit 1") + if id: + return id + # failed card queue too big? + if self.failedCount >= self.failedCardMax: + id = self.s.scalar( + "select id from failedCardsSoon limit 1") + if id: + return id + # distribute new cards? + if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: + if self._timeForNewCard(): + id = self._maybeGetNewCard() + if id: + return id + # card due for review? + id = self.s.scalar("select id from revCards limit 1") + if id: + return id + # new card last? + if self.newCardSpacing == NEW_CARDS_LAST: + id = self._maybeGetNewCard() + if id: + return id + if self.collapseTime: + # display failed cards early + id = self.s.scalar( + "select id from failedCardsSoon limit 1") + return id + + def getCardIds(self, limit): + """Return limit number of cards. +Caller is responsible for ensuring cards are not spaced.""" + def getCard(): + id = self.getCardId() + self.s.statement("update cards set isDue = 0 where id = :id", id=id) + return id + arr = [] + for i in range(limit): + c = getCard() + if c: + arr.append(c) + else: + break + return arr + + # Get card: helper functions + ########################################################################## + + def _timeForNewCard(self): + "True if it's time to display a new card when distributing." + # no cards for review, so force new + if not self.reviewCount: + return True + # force old if there are very high priority cards + if self.s.scalar( + "select 1 from cards where type = 1 and isDue = 1 " + "and priority = 4 limit 1"): + return False + if self.newCardModulus: + return self._dailyStats.reps % self.newCardModulus == 0 + else: + return False + + def _maybeGetNewCard(self): + "Get a new card, provided daily new card limit not exceeded." + if not self.newCountLeftToday: + return + return self._getNewCard() + + def _getNewCard(self): + "Return the next new card id, if exists." + if self.newCardOrder == 0: + return self.s.scalar( + "select id from acqCardsRandom limit 1") + else: + return self.s.scalar( + "select id from acqCardsOrdered limit 1") + + def cardFromId(self, id, orm=False): + "Given a card ID, return a card, and start the card timer." + if orm: + card = self.s.query(anki.cards.Card).get(id) + card.timerStopped = False + else: + card = anki.cards.Card() + card.fromDB(self.s, id) + card.genFuzz() + card.startTimer() + return card + + # Answering a card + ########################################################################## + + def answerCard(self, card, ease): + now = time.time() + oldState = self.cardState(card) + lastDelay = max(0, (time.time() - card.due) / 86400.0) + # update card details + card.lastInterval = card.interval + card.interval = self.nextInterval(card, ease) + card.lastDue = card.due + card.due = self.nextDue(card, ease, oldState) + card.isDue = 0 + card.lastFactor = card.factor + self.updateFactor(card, ease) + # spacing - first, we get the times of all other cards with the same + # fact + (minSpacing, spaceFactor) = self.s.first(""" +select models.initialSpacing, models.spacing from +facts, models where facts.modelId = models.id and facts.id = :id""", id=card.factId) + minOfOtherCards = self.s.scalar(""" +select min(interval) from cards +where factId = :fid and id != :id""", fid=card.factId, id=card.id) or 0 + if minOfOtherCards: + space = min(minOfOtherCards, card.interval) + else: + space = 0 + space = space * spaceFactor * 86400.0 + space = max(minSpacing, space) + space += time.time() + self.s.statement(""" +update cards set +spaceUntil = :space, +combinedDue = max(:space, due), +modified = :now, +isDue = 0 +where id != :id and factId = :factId""", + id=card.id, space=space, now=now, factId=card.factId) + card.spaceUntil = 0 + # card stats + anki.cards.Card.updateStats(card, ease, oldState) + card.toDB(self.s) + # global/daily stats + anki.stats.updateAllStats(self.s, self._globalStats, self._dailyStats, + card, ease, oldState) + # review history + entry = CardHistoryEntry(card, ease, lastDelay) + entry.writeSQL(self.s) + self.modified = now + # update isDue for failed cards + self.markExpiredCardsDue() + # invalidate counts + self._countsDirty = True + + # Queue/cache management + ########################################################################## + + def rebuildTypes(self, where=""): + "Rebuild the type cache. Only necessary on upgrade." + self.s.statement(""" +update cards +set type = (case +when successive = 0 and reps != 0 +then 0 -- failed +when successive != 0 and reps != 0 +then 1 -- review +else 2 -- new +end)""" + where) + + def markExpiredCardsDue(self): + "Mark expired cards due, and update their relativeDelay." + self.s.statement("""update cards +set isDue = 1, relativeDelay = interval / (strftime("%s", "now") - due + 1) +where isDue = 0 and priority in (1,2,3,4) and combinedDue < :now""", + now=time.time()) + + def updateRelativeDelays(self): + "Update relative delays for expired cards." + self.s.statement("""update cards +set relativeDelay = interval / (strftime("%s", "now") - due + 1) +where isDue = 1""") + + def rebuildQueue(self): + "Update relative delays based on current time." + self.updateRelativeDelays() + self.markExpiredCardsDue() + # cache global/daily stats + self._globalStats = globalStats(self.s) + self._dailyStats = dailyStats(self.s) + # invalid card count + self._countsDirty = True + # determine new card distribution + if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: + if self.newCountLeftToday: + self.newCardModulus = ((self.newCountLeftToday + self.reviewCount) + / self.newCountLeftToday) + # if there are cards to review, ensure modulo >= 2 + if self.reviewCount: + self.newCardModulus = max(2, self.newCardModulus) + else: + self.newCardModulus = 0 + # determine starting factor for new cards + self.averageFactor = (self.s.scalar( + "select avg(factor) from cards where reps > 0") + or Deck.initialFactor) + + # Interval management + ########################################################################## + + def nextInterval(self, card, ease): + "Return the next interval for CARD given EASE." + delay = self._adjustedDelay(card, ease) + # if interval is less than mid interval, use presets + if card.interval < self.midIntervalMin: + if ease < 2: + interval = NEW_INTERVAL + elif ease == 2: + interval = random.uniform(self.hardIntervalMin, + self.hardIntervalMax) + elif ease == 3: + interval = random.uniform(self.midIntervalMin, + self.midIntervalMax) + elif ease == 4: + interval = random.uniform(self.easyIntervalMin, + self.easyIntervalMax) + elif ease == 0: + interval = NEW_INTERVAL + else: + # otherwise, multiply the old interval by a factor + if ease == 1: + factor = 1 / card.factor / 2.0 + interval = card.interval * factor + elif ease == 2: + factor = 1.2 + interval = (card.interval + delay/4) * factor + elif ease == 3: + factor = card.factor + interval = (card.interval + delay/2) * factor + elif ease == 4: + factor = card.factor * self.factorFour + interval = (card.interval + delay) * factor + interval *= card.fuzz + if self.maxScheduleTime: + interval = min(interval, self.maxScheduleTime) + return interval + + def nextDue(self, card, ease, oldState): + "Return time when CARD will expire given EASE." + if ease == 0: + due = self.delay0 + elif ease == 1 and oldState != 'mature': + due = self.delay1 + elif ease == 1: + due = self.delay2 + else: + due = card.interval * 86400.0 + return due + time.time() + + def updateFactor(self, card, ease): + "Update CARD's factor based on EASE." + card.lastFactor = card.factor + if not card.reps: + # card is new, inherit beginning factor + card.factor = self.averageFactor + if self.cardIsBeingLearnt(card) and ease in [0, 1, 2]: + # only penalize failures after success when starting + if card.successive and ease != 2: + card.factor -= 0.20 + elif ease in [0, 1]: + card.factor -= 0.20 + elif ease == 2: + card.factor -= 0.15 + elif ease == 4: + card.factor += 0.10 + card.factor = max(1.3, card.factor) + + def _adjustedDelay(self, card, ease): + "Return an adjusted delay value for CARD based on EASE." + if self.cardIsNew(card): + return 0 + return max(0, (time.time() - card.due) / 86400.0) + + def resetCards(self, ids): + "Reset progress on cards in IDS." + self.s.statement(""" +update cards set interval = :new, lastInterval = 0, lastDue = 0, +factor = 2.5, reps = 0, successive = 0, averageTime = 0, reviewTime = 0, +youngEase0 = 0, youngEase1 = 0, youngEase2 = 0, youngEase3 = 0, +youngEase4 = 0, matureEase0 = 0, matureEase1 = 0, matureEase2 = 0, +matureEase3 = 0,matureEase4 = 0, yesCount = 0, noCount = 0, +spaceUntil = 0, relativeDelay = 0, isDue = 0, type = 2, +combinedDue = created, modified = :now, due = created +where id in %s""" % ids2str(ids), now=time.time(), new=NEW_INTERVAL) + self.flushMod() + + # Times + ########################################################################## + + def nextDueMsg(self): + next = self.earliestTime() + if next: + newCardsTomorrow = min(self.newCount, self.newCardsPerDay) + msg = _('''\ +At the same time tomorrow:

+- There will be %(wait)d cards waiting for review
+- There will be %(new)d + +new cards waiting''') % { + 'new': newCardsTomorrow, + 'wait': self.cardsDueBy(time.time() + 86400) + } + if next - time.time() > 86400 and not newCardsTomorrow: + msg = (_("The next card will be shown in %s") % + self.earliestTimeStr()) + else: + msg = _("The deck is empty. Please add some cards.") + return msg + + def earliestTime(self): + """Return the time of the earliest card. +This may be in the past if the deck is not finished. +If the deck has no (enabled) cards, return None. +Ignore new cards.""" + return self.s.scalar(""" +select combinedDue from cards where priority != 0 and type != 2 +order by combinedDue limit 1""") + + def earliestTimeStr(self, next=None): + """Return the relative time to the earliest card as a string.""" + if next == None: + next = self.earliestTime() + if not next: + return _("unknown") + diff = next - time.time() + return anki.utils.fmtTimeSpan(diff) + + def cardsDueBy(self, time): + "Number of cards due at TIME. Ignore new cards" + return self.s.scalar(""" +select count(id) from cards where combinedDue < :time +and priority != 0 and type != 2""", time=time) + + def nextIntervalStr(self, card, ease): + "Return the next interval for CARD given EASE as a string." + delay = self._adjustedDelay(card, ease) + if card.due > time.time() and ease < 2: + # the card is not yet due, and we are in the final drill + return _("a short time") + if ease < 2: + interval = self.nextDue(card, ease, self.cardState(card)) - time.time() + elif card.interval < self.midIntervalMin: + if ease == 2: + interval = [self.hardIntervalMin, self.hardIntervalMax] + elif ease == 3: + interval = [self.midIntervalMin, self.midIntervalMax] + else: + interval = [self.easyIntervalMin, self.easyIntervalMax] + interval[0] = interval[0] * 86400.0 + interval[1] = interval[1] * 86400.0 + if interval[0] != interval[1]: + return anki.utils.fmtTimeSpanPair(*interval) + interval = interval[0] + else: + interval = self.nextInterval(card, ease) * 86400.0 + return anki.utils.fmtTimeSpan(interval) + + def deckFinishedMsg(self): + return _(''' +

Congratulations!

You have finished the deck for now.

+%(next)s +

+- There are %(waiting)d + +spaced cards.
+- There are %(suspended)d + +suspended cards.''') % { + "next": self.nextDueMsg(), + "suspended": self.suspendedCardCount(), + "waiting": self.spacedCardCount() + } + + # Priorities + ########################################################################## + + def updateAllPriorities(self, extraExcludes=[], where=""): + "Update all card priorities if changed." + now = time.time() + t = time.time() + newPriorities = [] + tagsList = self.tagsList(where) + if not tagsList: + return + tagCache = self.genTagCache() + for e in extraExcludes: + tagCache['suspended'][e] = 1 + for (cardId, tags, oldPriority) in tagsList: + newPriority = self.priorityFromTagString(tags, tagCache) + if newPriority != oldPriority: + newPriorities.append({"id": cardId, "pri": newPriority}) + # update db + self.s.execute(text( + "update cards set priority = :pri where cards.id = :id"), + newPriorities) + self.s.execute("update cards set isDue = 0 where priority = 0") + + def updatePriority(self, card): + "Update priority on a single card." + tagCache = self.genTagCache() + tags = (card.tags + "," + card.fact.tags + "," + + card.fact.model.tags + "," + card.cardModel.name) + p = self.priorityFromTagString(tags, tagCache) + if p != card.priority: + card.priority = p + if p == 0: + card.isDue = 0 + self.s.flush() + + def priorityFromTagString(self, tagString, tagCache): + tags = parseTags(tagString.lower()) + for tag in tags: + if tag in tagCache['suspended']: + return PRIORITY_NONE + for tag in tags: + if tag in tagCache['high']: + return PRIORITY_HIGH + for tag in tags: + if tag in tagCache['med']: + return PRIORITY_MED + for tag in tags: + if tag in tagCache['low']: + return PRIORITY_LOW + return PRIORITY_NORM + + def genTagCache(self): + "Cache tags for quick lookup. Return dict." + d = {} + t = parseTags(self.suspended.lower()) + d['suspended'] = dict([(k, 1) for k in t]) + t = parseTags(self.highPriority.lower()) + d['high'] = dict([(k, 1) for k in t]) + t = parseTags(self.medPriority.lower()) + d['med'] = dict([(k, 1) for k in t]) + t = parseTags(self.lowPriority.lower()) + d['low'] = dict([(k, 1) for k in t]) + return d + + # Card/fact counts - all in deck, not just due + ########################################################################## + + def cardCount(self): + return self.s.scalar( + "select count(id) from cards") + + def factCount(self): + return self.s.scalar( + "select count(id) from facts") + + def suspendedCardCount(self): + return self.s.scalar( + "select count(id) from cards where priority = 0") + + def seenCardCount(self): + return self.s.scalar( + "select count(id) from cards where reps != 0") + + def newCardCount(self): + return self.s.scalar( + "select count(id) from cards where reps = 0") + + # Counts related to due cards + ########################################################################## + + def newCardsToday(self): + return (self._dailyStats.newEase0 + + self._dailyStats.newEase1 + + self._dailyStats.newEase2 + + self._dailyStats.newEase3 + + self._dailyStats.newEase4) + + def updateCounts(self): + "Update failed/rev/new counts if cache is dirty." + if self._countsDirty: + self._failedCount = self.s.scalar(""" +select count(id) from failedCardsSoon""") + self._failedDueNowCount = self.s.scalar(""" +select count(id) from failedCardsNow""") + self._reviewCount = self.s.scalar( + "select count(isDue) from cards where isDue = 1 and type = 1") + self._newCount = self.s.scalar( + "select count(isDue) from cards where isDue = 1 and type = 2") + if getattr(self, '_dailyStats', None): + self._newCountLeftToday = max(min( + self._newCount, self.newCardsPerDay - + self.newCardsToday()), 0) + self._countsDirty = False + + def _getFailedCount(self): + self.updateCounts() + return self._failedCount + failedCount = property(_getFailedCount) + + def _getFailedDueNowCount(self): + self.updateCounts() + return self._failedDueNowCount + failedDueNowCount = property(_getFailedDueNowCount) + + def _getReviewCount(self): + self.updateCounts() + return self._reviewCount + reviewCount = property(_getReviewCount) + + def _getNewCount(self): + self.updateCounts() + return self._newCount + newCount = property(_getNewCount) + + def _getNewCountLeftToday(self): + self.updateCounts() + return self._newCountLeftToday + newCountLeftToday = property(_getNewCountLeftToday) + + def spacedCardCount(self): + return self.s.scalar(""" +select count(cards.id) from cards where +priority != 0 and due < :now and spaceUntil > :now""", + now=time.time()) + + def isEmpty(self): + return self.cardCount() == 0 + + def matureCardCount(self): + return self.s.scalar( + "select count(id) from cards where interval >= :t ", + t=MATURE_THRESHOLD) + + def youngCardCount(self): + return self.s.scalar( + "select count(id) from cards where interval < :t " + "and reps != 0", t=MATURE_THRESHOLD) + + # Card predicates + ########################################################################## + + def cardState(self, card): + if self.cardIsNew(card): + return "new" + elif card.interval > MATURE_THRESHOLD: + return "mature" + return "young" + + def cardIsNew(self, card): + "True if a card has never been seen before." + return card.reps == 0 + + def cardIsBeingLearnt(self, card): + "True if card should use present intervals." + return card.interval < self.easyIntervalMin + + def cardIsYoung(self, card): + "True if card is not new and not mature." + return (not self.cardIsNew(card) and + not self.cardIsMature(card)) + + def cardIsMature(self, card): + return card.interval >= MATURE_THRESHOLD + + # Stats + ########################################################################## + + def getStats(self): + "Return some commonly needed stats." + stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats) + # add scheduling related stats + stats['new'] = self.newCountLeftToday + stats['failed'] = self.failedCount + stats['successive'] = self.reviewCount + #stats['old'] = stats['failed'] + stats['successive'] + if stats['dAverageTime']: + if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: + count = stats['successive'] + stats['new'] + elif self.newCardSpacing == NEW_CARDS_LAST: + count = stats['successive'] or stats['new'] + stats['timeLeft'] = anki.utils.fmtTimeSpan( + stats['dAverageTime'] * count, pad=0, point=1) + else: + stats['timeLeft'] = _("Unknown") + return stats + + def queueForCard(self, card): + "Return the queue the current card is in." + if self.cardIsNew(card): + if card.priority == 4: + return "rev" + else: + return "new" + elif card.successive == 0: + return "failed" + elif card.reps: + return "rev" + else: + sys.stderr.write("couldn't determine queue for %s" % + `card.__dict__`) + + # Facts + ########################################################################## + + def newFact(self): + "Return a new fact with the current model." + return anki.facts.Fact(self.currentModel) + + def addFact(self, fact): + "Add a fact to the deck. Return list of new cards." + if not fact.model: + fact.model = self.currentModel + # the session may have been cleared, so refresh model + fact.model = self.s.query(Model).get(fact.model.id) + # validate + fact.assertValid() + fact.assertUnique(self.s) + # and associated cards + n = 0 + cards = [] + self.s.save(fact) + self.flushMod() + for cardModel in fact.model.cardModels: + if cardModel.active: + card = anki.cards.Card(fact, cardModel) + self.flushMod() + self.updatePriority(card) + cards.append(card) + # keep track of last used tags for convenience + self.lastTags = fact.tags + self.setModified() + self._countsDirty = True + return cards + + def addMissingCards(self, fact): + "Caller must flush first, flushMod after, and rebuild priorities." + for cardModel in fact.model.cardModels: + if cardModel.active: + if self.s.scalar(""" +select count(id) from cards +where factId = :fid and cardModelId = :cmid""", + fid=fact.id, cmid=cardModel.id) == 0: + card = anki.cards.Card(fact, cardModel) + # not added to queue + self.setModified() + + def factIsInvalid(self, fact): + "True if existing fact is invalid. Returns the error." + try: + fact.assertValid() + fact.assertUnique(self.s) + except FactInvalidError, e: + return e + + def factUseCount(self, factId): + "Return number of cards referencing a given fact id." + return self.s.scalar("select count(id) from cards where factId = :id", + id=factId) + + def deleteFact(self, factId): + "Delete a fact. Removes any associated cards. Don't flush." + self.s.flush() + # remove any remaining cards + self.s.statement("insert into cardsDeleted select id, :time " + "from cards where factId = :factId", + time=time.time(), factId=factId) + self.s.statement("delete from cards where factId = :id", id=factId) + # and then the fact + self.s.statement("delete from facts where id = :id", id=factId) + self.s.statement("delete from fields where factId = :id", id=factId) + self.s.statement("insert into factsDeleted values (:id, :time)", + id=factId, time=time.time()) + self.setModified() + + def deleteFacts(self, ids): + "Bulk delete facts by ID. Assume any cards have already been removed." + if not ids: + return + self.s.flush() + now = time.time() + strids = ids2str(ids) + self.s.statement("delete from facts where id in %s" % strids) + self.s.statement("delete from fields where factId in %s" % strids) + data = [{'id': id, 'time': now} for id in ids] + self.s.statements("insert into factsDeleted values (:id, :time)", data) + self.setModified() + + def deleteDanglingFacts(self): + "Delete any facts without cards. Return deleted ids." + ids = self.s.column0(""" +select facts.id from facts +where facts.id not in (select factId from cards)""") + self.deleteFacts(ids) + return ids + + # Cards + ########################################################################## + + def deleteCard(self, id): + "Delete a card given its id. Delete any unused facts. Don't flush." + self.s.flush() + factId = self.s.scalar("select factId from cards where id=:id", id=id) + self.s.statement("delete from cards where id = :id", id=id) + self.s.statement("insert into cardsDeleted values (:id, :time)", + id=id, time=time.time()) + if factId and not self.factUseCount(factId): + self.deleteFact(factId) + self.setModified() + + def deleteCards(self, ids): + "Bulk delete cards by ID." + if not ids: + return + self.s.flush() + now = time.time() + strids = ids2str(ids) + # grab fact ids + factIds = self.s.column0("select factId from cards where id in %s" + % strids) + # drop from cards + self.s.statement("delete from cards where id in %s" % strids) + # note deleted + data = [{'id': id, 'time': now} for id in ids] + self.s.statements("insert into cardsDeleted values (:id, :time)", data) + # remove any dangling facts + self.deleteDanglingFacts() + self.setModified() + + # Models + ########################################################################## + + def addModel(self, model): + if model not in self.models: + self.models.append(model) + self.currentModel = model + self.flushMod() + + def deleteModel(self, model): + "Delete MODEL, and delete any referencing cards/facts. Maybe flush." + if self.s.scalar("select count(id) from models where id=:id", + id=model.id): + # delete facts/cards + self.currentModel + self.deleteCards(self.s.column0(""" +select cards.id from cards, facts where +facts.modelId = :id and +facts.id = cards.factId""", id=model.id)) + # then the model + self.models.remove(model) + self.s.delete(model) + self.s.flush() + if self.currentModel == model: + self.currentModel = self.models[0] + self.s.statement("insert into modelsDeleted values (:id, :time)", + id=model.id, time=time.time()) + self.flushMod() + self.refresh() + self.setModified() + + def modelUseCount(self, model): + "Return number of facts using model." + return self.s.scalar("select count(facts.modelId) from facts " + "where facts.modelId = :id", + id=model.id) + + def deleteEmptyModels(self): + for model in self.models: + if not self.modelUseCount(model): + self.deleteModel(model) + + def modelsGroupedByName(self): + "Return hash of name -> [id, cardModelIds, fieldIds]" + l = self.s.all("select name, id from models order by created") + models = {} + for m in l: + cms = self.s.column0(""" +select id from cardModels where modelId = :id order by ordinal""", id=m[1]) + fms = self.s.column0(""" +select id from fieldModels where modelId = :id order by ordinal""", id=m[1]) + if m[0] in models: + models[m[0]].append((m[1], cms, fms)) + else: + models[m[0]] = [(m[1], cms, fms)] + return models + + def canMergeModels(self): + models = self.modelsGroupedByName() + toProcess = [] + msg = "" + for (name, ids) in models.items(): + if len(ids) > 1: + cms = len(ids[0][1]) + fms = len(ids[0][2]) + for id in ids[1:]: + if len(id[1]) != cms: + msg = (_( + "Model '%s' has wrong card model count") % name) + break + if len(id[2]) != fms: + msg = (_( + "Model '%s' has wrong field model count") % name) + break + toProcess.append((name, ids)) + if msg: + return ("no", msg) + return ("ok", toProcess) + + def mergeModels(self, toProcess): + for (name, ids) in toProcess: + (id1, cms1, fms1) = ids[0] + for (id2, cms2, fms2) in ids[1:]: + self.mergeModel((id1, cms1, fms1), + (id2, cms2, fms2)) + + def mergeModel(self, m1, m2): + "Given two model ids, merge m2 into m1." + (id1, cms1, fms1) = m1 + (id2, cms2, fms2) = m2 + self.s.flush() + # cards + for n in range(len(cms1)): + self.s.statement(""" +update cards set +modified = strftime("%s", "now"), +cardModelId = :new where cardModelId = :old""", + new=cms1[n], old=cms2[n]) + # facts + self.s.statement(""" +update facts set +modified = strftime("%s", "now"), +modelId = :new where modelId = :old""", + new=id1, old=id2) + # fields + for n in range(len(fms1)): + self.s.statement(""" +update fields set +fieldModelId = :new where fieldModelId = :old""", + new=fms1[n], old=fms2[n]) + # delete m2 + model = [m for m in self.models if m.id == id2][0] + self.deleteModel(model) + self.refresh() + + # Fields + ########################################################################## + + def allFields(self): + "Return a list of all possible fields across all models." + return self.s.column0("select distinct name from fieldmodels") + + def deleteFieldModel(self, model, field): + self.s.statement("delete from fields where fieldModelId = :id", + id=field.id) + self.s.statement("update facts set modified = :t where modelId = :id", + id=model.id, t=time.time()) + model.fieldModels.remove(field) + # update q/a formats + for cm in model.cardModels: + cm.qformat = cm.qformat.replace("%%(%s)s" % field.name, "") + cm.aformat = cm.aformat.replace("%%(%s)s" % field.name, "") + model.setModified() + self.flushMod() + + def addFieldModel(self, model, field): + "Add FIELD to MODEL and update cards." + model.addFieldModel(field) + # commit field to disk + self.s.flush() + self.s.statement(""" +insert into fields (factId, fieldModelId, ordinal, value) +select facts.id, :fmid, :ordinal, "" from facts +where facts.modelId = :mid""", fmid=field.id, mid=model.id, ordinal=field.ordinal) + # ensure facts are marked updated + self.s.statement(""" +update facts set modified = :t where modelId = :mid""" + , t=time.time(), mid=model.id) + model.setModified() + self.flushMod() + + def renameFieldModel(self, model, field, newName): + "Change FIELD's name in MODEL and update FIELD in all facts." + for cm in model.cardModels: + cm.qformat = cm.qformat.replace( + "%%(%s)s" % field.name, "%%(%s)s" % newName) + cm.aformat = cm.aformat.replace( + "%%(%s)s" % field.name, "%%(%s)s" % newName) + field.name = newName + model.setModified() + self.flushMod() + + def fieldModelUseCount(self, fieldModel): + "Return the number of cards using fieldModel." + return self.s.scalar(""" +select count(id) from fields where +fieldModelId = :id and value != "" +""", id=fieldModel.id) + + def rebuildFieldOrdinals(self, modelId, ids): + """Update field ordinal for all fields given field model IDS. +Caller must update model modtime.""" + self.s.flush() + strids = ids2str(ids) + self.s.statement(""" +update fields +set ordinal = (select ordinal from fieldModels where id = fieldModelId) +where fields.fieldModelId in %s""" % strids) + # dirty associated facts + self.s.statement(""" +update facts +set modified = strftime("%s", "now") +where modelId = :id""", id=modelId) + self.flushMod() + + # Card models + ########################################################################## + + def cardModelUseCount(self, cardModel): + "Return the number of cards using cardModel." + return self.s.scalar(""" +select count(id) from cards where +cardModelId = :id""", id=cardModel.id) + + def deleteCardModel(self, model, cardModel): + "Delete all cards that use CARDMODEL from the deck." + cards = self.s.column0("select id from cards where cardModelId = :id", + id=cardModel.id) + for id in cards: + self.deleteCard(id) + model.cardModels.remove(cardModel) + model.setModified() + self.flushMod() + + def updateCardsFromModel(self, cardModel): + "Update all card question/answer when model changes." + ids = self.s.all(""" +select cards.id, cards.cardModelId, cards.factId from +cards, facts, cardmodels where +cards.factId = facts.id and +facts.modelId = cardModels.modelId and +cards.cardModelId = :id""", id=cardModel.id) + if not ids: + return + self.updateCardQACache(ids) + + def updateCardQACache(self, ids, dirty=True): + "Given a list of (cardId, cardModelId, factId), update q/a cache." + if dirty: + mod = ", modified = %f" % time.time() + else: + mod = "" + cms = self.s.query(CardModel).all() + for cm in cms: + pend = [{'q': cm.renderQASQL('q', fid), + 'a': cm.renderQASQL('a', fid), + 'id': cid} + for (cid, cmid, fid) in ids if cmid == cm.id] + if pend: + self.s.execute(""" + update cards set + question = :q, + answer = :a + %s + where id = :id""" % mod, pend) + + def rebuildCardOrdinals(self, ids): + "Update all card models in IDS. Caller must update model modtime." + self.s.flush() + strids = ids2str(ids) + self.s.statement(""" +update cards set +ordinal = (select ordinal from cardModels where id = cardModelId), +modified = :now +where cardModelId in %s""" % strids, now=time.time()) + self.flushMod() + + # Tags + ########################################################################## + + def tagsList(self, where=""): + "Return a list of (cardId, allTags, priority)" + return self.s.all(""" +select cards.id, cards.tags || "," || facts.tags || "," || models.tags || "," || +cardModels.name, cards.priority from cards, facts, models, cardModels where +cards.factId == facts.id and facts.modelId == models.id +and cards.cardModelId = cardModels.id %s""" % where) + + def allTags(self): + "Return a hash listing tags in model, fact and cards." + return list(set(parseTags(",".join([x[1] for x in self.tagsList()])))) + + def cardTags(self, ids): + return self.s.all(""" +select id, tags from cards +where id in %s""" % ids2str(ids)) + + def factTags(self, ids): + return self.s.all(""" +select id, tags from facts +where id in %s""" % ids2str(ids)) + + def addCardTags(self, ids, tags, idfunc=None, table="cards"): + if not idfunc: + idfunc=self.cardTags + tlist = idfunc(ids) + newTags = parseTags(tags) + now = time.time() + pending = [] + for (id, tags) in tlist: + oldTags = parseTags(tags) + tmpTags = list(set(oldTags + newTags)) + if tmpTags != oldTags: + pending.append( + {'id': id, 'now': now, 'tags': ", ".join(tmpTags)}) + self.s.statements(""" +update %s set +tags = :tags, +modified = :now +where id = :id""" % table, pending) + self.flushMod() + + def addFactTags(self, ids, tags): + self.addCardTags(ids, tags, idfunc=self.factTags, table="facts") + + def deleteCardTags(self, ids, tags, idfunc=None, table="cards"): + if not idfunc: + idfunc=self.cardTags + tlist = idfunc(ids) + newTags = parseTags(tags) + now = time.time() + pending = [] + for (id, tags) in tlist: + oldTags = parseTags(tags) + tmpTags = oldTags[:] + for tag in newTags: + try: + tmpTags.remove(tag) + except ValueError: + pass + if tmpTags != oldTags: + pending.append( + {'id': id, 'now': now, 'tags': ", ".join(tmpTags)}) + self.s.statements(""" +update %s set +tags = :tags, +modified = :now +where id = :id""" % table, pending) + self.flushMod() + + def deleteFactTags(self, ids, tags): + self.deleteCardTags(ids, tags, idfunc=self.factTags, table="facts") + + # File-related + ########################################################################## + + def name(self): + n = os.path.splitext(os.path.basename(self.path))[0] + n = re.sub("[^-A-Za-z0-9_+<>[]() ]", "", n) + return n + + # Media + ########################################################################## + + def mediaDir(self, create=False): + "Return the media directory if exists. None if couldn't create." + if not self.path: + return None + dir = re.sub("(?i)\.(anki)$", ".media", self.path) + if not os.path.exists(dir) and create: + try: + os.mkdir(dir) + except OSError: + # permission denied + return None + if not os.path.exists(dir): + return None + return dir + + def addMedia(self, path): + """Add PATH to the media directory. +Return new path, relative to media dir.""" + return anki.media.copyToMedia(self, path) + + def renameMediaDir(self, oldPath): + "Copy oldPath to our current media dir. " + assert os.path.exists(oldPath) + newPath = self.mediaDir(create=True) + # copytree doesn't want the dir to exist + os.rmdir(newPath) + shutil.copytree(oldPath, newPath) + + # DB helpers + ########################################################################## + + def save(self): + "Commit any pending changes to disk." + if self.lastLoaded == self.modified: + return + self.lastLoaded = self.modified + self.s.commit() + + def close(self): + self.s.rollback() + self.s.clear() + self.engine.dispose() + + def rollback(self): + "Roll back the current transaction and reset session state." + self.s.rollback() + self.s.clear() + self.refresh() + + def refresh(self): + "Flush, invalidate all objects from cache and reload." + self.s.flush() + self.s.clear() + self.s.update(self) + self.s.refresh(self) + + def openSession(self): + "Open a new session. Assumes old session is already closed." + self.s = SessionHelper(self.Session(), lock=self.needLock) + self.refresh() + + def closeSession(self): + "Close the current session, saving any changes. Do nothing if no session." + if self.s: + self.save() + try: + self.s.expunge(self) + except: + import sys + sys.stderr.write("ERROR expunging deck..\n") + self.s.close() + self.s = None + + def setModified(self, newTime=None): + self.modified = newTime or time.time() + + def flushMod(self): + "Mark modified and flush to DB." + self.setModified() + self.s.flush() + + def saveAs(self, newPath): + oldMediaDir = self.mediaDir() + # flush old deck + self.s.flush() + # remove new deck if it exists + try: + os.unlink(newPath) + except OSError: + pass + # create new deck + newDeck = DeckStorage.Deck(newPath) + # attach current db to new + s = newDeck.s.statement + s("pragma read_uncommitted = 1") + s("attach database :path as old", path=self.path) + # copy all data + s("delete from decks") + s("delete from stats") + s("insert into decks select * from old.decks") + s("insert into fieldModels select * from old.fieldModels") + s("insert into modelsDeleted select * from old.modelsDeleted") + s("insert into cardModels select * from old.cardModels") + s("insert into facts select * from old.facts") + s("insert into fields select * from old.fields") + s("insert into cards select * from old.cards") + s("insert into factsDeleted select * from old.factsDeleted") + s("insert into reviewHistory select * from old.reviewHistory") + s("insert into cardsDeleted select * from old.cardsDeleted") + s("insert into models select * from old.models") + s("insert into stats select * from old.stats") + # detach old db and commit + s("detach database old") + newDeck.s.commit() + # close ourself, rebuild queue + self.s.close() + newDeck.refresh() + newDeck.rebuildQueue() + # move media + if oldMediaDir: + newDeck.renameMediaDir(oldMediaDir) + # and return the new deck object + return newDeck + + # DB maintenance + ########################################################################## + + def fixIntegrity(self): + "Responsibility of caller to call rebuildQueue()" + if self.s.scalar("pragma integrity_check") != "ok": + return _("Database file damaged. Restore from backup.") + # ensure correct views and indexes are available + DeckStorage._addViews(self) + DeckStorage._addIndices(self) + problems = [] + # does the user have a model? + if not self.s.scalar("select count(id) from models"): + self.addModel(BasicModel()) + problems.append(_("Deck was missing a model")) + # is currentModel pointing to a valid model? + if not self.s.all(""" +select decks.id from decks, models where +decks.currentModelId = models.id"""): + self.currentModelId = self.models[0].id + problems.append(_("The current model didn't exist")) + # facts missing a field? + ids = self.s.column0(""" +select distinct facts.id from facts, fieldModels where +facts.modelId = fieldModels.modelId and fieldModels.id not in +(select fieldModelId from fields where factId = facts.id)""") + if ids: + self.deleteFacts(ids) + problems.append(_("Deleted %d facts with missing fields") % + len(ids)) + # cards missing a fact? + ids = self.s.column0(""" +select id from cards where factId not in (select id from facts)""") + if ids: + self.deleteCards(ids) + problems.append(_("Deleted %d cards with missing fact") % + len(ids)) + # cards missing a card model? + ids = self.s.column0(""" +select id from cards where cardModelId not in +(select id from cardModels)""") + if ids: + self.deleteCards(ids) + problems.append(_("Deleted %d cards with no card model" % + len(ids))) + # facts missing a card? + ids = self.deleteDanglingFacts() + if ids: + problems.append(_("Deleted %d facts with no cards" % + len(ids))) + # dangling fields? + ids = self.s.column0(""" +select id from fields where factId not in (select id from facts)""") + if ids: + self.s.statement( + "delete from fields where id in %s" % ids2str(ids)) + problems.append(_("Deleted %d dangling fields") % len(ids)) + self.s.flush() + # fix problems with cards being scheduled when not due + self.s.statement("update cards set isDue = 0") + # fix problems with conflicts on merge + self.s.statement("update fields set id = random()") + # fix any priorities + self.updateAllPriorities() + # fix problems with stripping html + fields = self.s.all("select id, value from fields") + newFields = [] + for (id, value) in fields: + newFields.append({'id': id, 'value': tidyHTML(value)}) + self.s.statements( + "update fields set value=:value where id=:id", + newFields) + # regenerate question/answer cache + cms = self.s.query(CardModel).all() + for cm in cms: + self.updateCardsFromModel(cm) + self.s.expunge(cm) + # mark everything changed to force sync + self.s.flush() + self.s.statement("update cards set modified = :t", t=time.time()) + self.s.statement("update facts set modified = :t", t=time.time()) + self.s.statement("update models set modified = :t", t=time.time()) + self.lastSync = 0 + # update deck and save + self.flushMod() + self.save() + self.refresh() + self.rebuildTypes() + self.rebuildQueue() + if problems: + return "\n".join(problems) + return "ok" + + def optimize(self): + oldSize = os.stat(self.path)[stat.ST_SIZE] + self.s.statement("vacuum") + self.s.statement("analyze") + newSize = os.stat(self.path)[stat.ST_SIZE] + return oldSize - newSize + +########################################################################## + +mapper(Deck, decksTable, properties={ + 'currentModel': relation(anki.models.Model, primaryjoin= + decksTable.c.currentModelId == + anki.models.modelsTable.c.id), + 'models': relation(anki.models.Model, post_update=True, + primaryjoin= + decksTable.c.id == + anki.models.modelsTable.c.deckId), + }) + +# Deck storage +########################################################################## + +class DeckStorage(object): + + backupDir = os.path.expanduser("~/.anki/backups") + numBackups = 100 + newDeckDir = "~" + + def newDeckPath(): + # create ~/mydeck(N).anki + n = 2 + path = os.path.expanduser( + os.path.join(DeckStorage.newDeckDir, "mydeck.anki")) + while os.path.exists(path): + path = os.path.expanduser( + os.path.join(DeckStorage.newDeckDir, "mydeck%d.anki") % n) + n += 1 + return path + newDeckPath = staticmethod(newDeckPath) + + def Deck(path=None, rebuild=True, backup=True, lock=True): + "Create a new deck or attach to an existing one." + # generate a temp name if necessary + if path is None: + path = DeckStorage.newDeckPath() + create = True + if path != -1: + if isinstance(path, types.UnicodeType): + path = path.encode(sys.getfilesystemencoding()) + path = os.path.abspath(path) + #print "using path", path + if os.path.exists(path): + # attach + if not os.access(path, os.R_OK | os.W_OK): + raise DeckAccessError(_("Can't read/write deck")) + create = False + # attach and sync/fetch deck - first, to unicode + if not isinstance(path, types.UnicodeType): + path = unicode(path, sys.getfilesystemencoding()) + # sqlite needs utf8 + (engine, session) = DeckStorage._attach(path.encode("utf-8"), create) + s = session() + try: + if create: + deck = DeckStorage._init(s) + else: + if s.scalar("select version from decks limit 1") < 5: + # add missing deck field + s.execute(""" +alter table decks add column newCardsPerDay integer not null default 20""") + if s.scalar("select version from decks limit 1") < 6: + s.execute(""" +alter table decks add column sessionRepLimit integer not null default 100""") + s.execute(""" +alter table decks add column sessionTimeLimit integer not null default 1800""") + deck = s.query(Deck).get(1) + # attach db vars + deck.path = path + deck.engine = engine + deck.Session = session + deck.needLock = lock + deck.s = SessionHelper(s, lock=lock) + if create: + # new-style file format + deck.s.execute("pragma legacy_file_format = off") + deck.s.execute("vacuum") + # add views/indices + DeckStorage._addViews(deck) + DeckStorage._addIndices(deck) + deck.s.statement("analyze") + deck._initVars() + else: + if backup: + DeckStorage.backup(deck.modified, path) + deck._initVars() + deck = DeckStorage._upgradeDeck(deck, path) + except OperationalError, e: + if (str(e.orig).startswith("database table is locked") or + str(e.orig).startswith("database is locked")): + raise DeckAccessError(_("File is in use by another process"), + type="inuse") + else: + raise e + # rebuild? + if rebuild: + deck.rebuildQueue() + deck.s.commit() + return deck + Deck = staticmethod(Deck) + + def _attach(path, create): + "Attach to a file, initializing DB" + if path == -1: + path = "sqlite:///:memory:" + else: + path = "sqlite:///" + path + engine = create_engine(path, + strategy='threadlocal', + connect_args={'timeout': 0}) + session = sessionmaker(bind=engine, + autoflush=False, + transactional=False) + try: + metadata.create_all(engine) + except DBAPIError, e: + engine.dispose() + if create: + raise DeckAccessError(_("Can't read/write deck")) + else: + raise DeckWrongFormatError("Deck is not in the right format") + return (engine, session) + _attach = staticmethod(_attach) + + def _init(s): + "Add a new deck to the database. Return saved deck." + deck = Deck() + s.save(deck) + s.flush() + return deck + _init = staticmethod(_init) + + def _addIndices(deck): + "Add indices to the DB." + # card queues + deck.s.statement(""" +create index if not exists ix_cards_markExpired on cards +(isDue, priority desc, combinedDue desc)""") + deck.s.statement(""" +create index if not exists ix_cards_failedIsDue on cards +(type, isDue, combinedDue)""") + deck.s.statement(""" +create index if not exists ix_cards_failedOrder on cards +(type, isDue, due)""") + deck.s.statement(""" +create index if not exists ix_cards_revisionOrder on cards +(type, isDue, priority desc, relativeDelay)""") + deck.s.statement(""" +create index if not exists ix_cards_newRandomOrder on cards +(priority desc, factId, ordinal)""") + deck.s.statement(""" +create index if not exists ix_cards_newOrderedOrder on cards +(priority desc, due)""") + # card spacing + deck.s.statement(""" +create index if not exists ix_cards_factId on cards (factId)""") + # stats + deck.s.statement(""" +create index if not exists ix_stats_typeDay on stats (type, day)""") + # fields + deck.s.statement(""" +create index if not exists ix_fields_factId on fields (factId)""") + deck.s.statement(""" +create index if not exists ix_fields_fieldModelId on fields (fieldModelId)""") + deck.s.statement(""" +create index if not exists ix_fields_value on fields (value)""") + # media + deck.s.statement(""" +create unique index if not exists ix_media_filename on media (filename)""") + # deletion tracking + deck.s.statement(""" +create index if not exists ix_cardsDeleted_cardId on cardsDeleted (cardId)""") + deck.s.statement(""" +create index if not exists ix_modelsDeleted_modelId on modelsDeleted (modelId)""") + deck.s.statement(""" +create index if not exists ix_factsDeleted_factId on factsDeleted (factId)""") + deck.s.statement(""" +create index if not exists ix_mediaDeleted_factId on mediaDeleted (mediaId)""") + _addIndices = staticmethod(_addIndices) + + def _addViews(deck): + "Add latest version of SQL views to DB." + s = deck.s + # old tables + s.statement("drop view if exists failedCards") + s.statement("drop view if exists acqCards") + s.statement("drop view if exists futureCards") + s.statement("drop view if exists typedCards") + s.statement("drop view if exists failedCards") + s.statement("drop view if exists failedCardsNow") + s.statement(""" +create view failedCardsNow as +select * from cards +where type = 0 and isDue = 1 +and combinedDue <= (strftime("%s", "now") + 1) +order by combinedDue +""") + s.statement("drop view if exists failedCardsSoon") + s.statement(""" +create view failedCardsSoon as +select * from cards +where type = 0 and priority != 0 +and combinedDue <= +(select max(delay0, delay1)+strftime("%s", "now")+1 +from decks) +order by modified +""") + s.statement("drop view if exists revCards") + s.statement(""" +create view revCards as +select * from cards where +type = 1 and isDue = 1 +order by type, isDue, priority desc, relativeDelay""") + s.statement("drop view if exists acqCardsRandom") + s.statement(""" +create view acqCardsRandom as +select * from cards +where type = 2 and isDue = 1 +order by priority desc, factId, ordinal""") + s.statement("drop view if exists acqCardsOrdered") + s.statement(""" +create view acqCardsOrdered as +select * from cards +where type = 2 and isDue = 1 +order by priority desc, due""") + _addViews = staticmethod(_addViews) + + def _upgradeDeck(deck, path): + "Upgrade deck to the latest version." + deck.path = path + if deck.version == 0: + # new columns + try: + deck.s.statement(""" + alter table cards add column spaceUntil float not null default 0""") + deck.s.statement(""" + alter table cards add column relativeDelay float not null default 0.0""") + deck.s.statement(""" + alter table cards add column isDue boolean not null default 0""") + deck.s.statement(""" + alter table cards add column type integer not null default 0""") + deck.s.statement(""" + alter table cards add column combinedDue float not null default 0""") + # update cards.spaceUntil based on old facts + deck.s.statement(""" + update cards + set spaceUntil = (select (case + when cards.id = facts.lastCardId + then 0 + else facts.spaceUntil + end) from cards as c, facts + where c.factId = facts.id + and cards.id = c.id)""") + deck.s.statement(""" + update cards + set combinedDue = max(due, spaceUntil) + """) + except: + print "failed to upgrade" + # rebuild with new file format + deck.s.execute("pragma legacy_file_format = off") + deck.s.execute("vacuum") + # add views/indices + DeckStorage._addViews(deck) + DeckStorage._addIndices(deck) + # rebuild type and delay cache + deck.rebuildTypes() + deck.rebuildQueue() + deck.s.commit() + # bump version + deck.version = 1 + deck.s.commit() + # optimize indices + deck.s.statement("analyze") + if deck.version == 1: + # fix indexes and views + deck.s.statement("drop index ix_cards_newRandomOrder") + deck.s.statement("drop index ix_cards_newOrderedOrder") + DeckStorage._addViews(deck) + DeckStorage._addIndices(deck) + deck.rebuildTypes() + # optimize indices + deck.s.statement("analyze") + deck.version = 2 + deck.s.commit() + if deck.version == 2: + # compensate for bug in 0.9.7 by rebuilding isDue and priorities + deck.s.statement("update cards set isDue = 0") + deck.updateAllPriorities() + # compensate for bug in early 0.9.x where fieldId was not unique + deck.s.statement("update fields set id = random()") + deck.version = 3 + deck.s.commit() + if deck.version == 3: + # remove conflicting and unused indexes + deck.s.statement("drop index if exists ix_cards_isDueCombined") + deck.s.statement("drop index if exists ix_facts_lastCardId") + deck.s.statement("drop index if exists ix_cards_successive") + deck.s.statement("drop index if exists ix_cards_priority") + deck.s.statement("drop index if exists ix_cards_reps") + deck.s.statement("drop index if exists ix_cards_due") + deck.s.statement("drop index if exists ix_stats_type") + deck.s.statement("drop index if exists ix_stats_day") + deck.s.statement("drop index if exists ix_factsDeleted_cardId") + deck.s.statement("drop index if exists ix_modelsDeleted_cardId") + DeckStorage._addIndices(deck) + deck.s.statement("analyze") + deck.version = 4 + deck.s.commit() + if deck.version == 4: + # decks field upgraded earlier + deck.version = 5 + deck.s.commit() + if deck.version == 5: + # new spacing + deck.newCardSpacing = NEW_CARDS_DISTRIBUTE + deck.version = 6 + deck.s.commit() + # low priority cards now stay in same queue + deck.rebuildTypes() + if deck.version == 6: + # removed 'new cards first' option, so order has changed + deck.newCardSpacing = NEW_CARDS_DISTRIBUTE + deck.version = 7 + deck.s.commit() + # 8 upgrade code removed as obsolete> + if deck.version < 9: + # back up the media dir again, just in case + shutil.copytree(deck.mediaDir(create=True), + deck.mediaDir() + "-old-%s" % + hash(time.time())) + # backup media + media = deck.s.all(""" +select filename, size, created, originalPath, description from media""") + # fix mediaDeleted definition + deck.s.execute("drop table mediaDeleted") + deck.s.execute("drop table media") + metadata.create_all(deck.engine) + # restore + h = [] + for row in media: + h.append({ + 'id': genID(), + 'filename': row[0], + 'size': row[1], + 'created': row[2], + 'originalPath': row[3], + 'description': row[4]}) + if h: + deck.s.statements(""" +insert into media values ( +:id, :filename, :size, :created, :originalPath, :description)""", h) + # rerun check + anki.media.rebuildMediaDir(deck, dirty=False) + # no need to track deleted media yet + deck.s.execute("delete from mediaDeleted") + deck.version = 9 + deck.s.commit() + return deck + _upgradeDeck = staticmethod(_upgradeDeck) + + def backup(modified, path): + # need a non-unicode path + path = path.encode(sys.getfilesystemencoding()) + backupDir = DeckStorage.backupDir.encode(sys.getfilesystemencoding()) + numBackups = DeckStorage.numBackups + def backupName(path, num): + path = os.path.abspath(path) + path = path.replace("\\", "!") + path = path.replace("/", "!") + path = path.replace(":", "") + path = os.path.join(backupDir, path) + path = re.sub("\.anki$", ".backup-%d.anki" % num, path) + return path + if not os.path.exists(backupDir): + os.makedirs(backupDir) + # if the mod time is identical, don't make a new backup + firstBack = backupName(path, 0) + if os.path.exists(firstBack): + s1 = int(modified) + s2 = int(os.stat(firstBack)[stat.ST_MTIME]) + if s1 == s2: + return + # remove the oldest backup if it exists + oldest = backupName(path, numBackups) + if os.path.exists(oldest): + os.chmod(oldest, 0666) + os.unlink(oldest) + # move all the other backups up one + for n in range(numBackups - 1, -1, -1): + name = backupName(path, n) + if os.path.exists(name): + newname = backupName(path, n+1) + if os.path.exists(newname): + os.chmod(newname, 0666) + os.unlink(newname) + os.rename(name, newname) + # save the current path + newpath = backupName(path, 0) + if os.path.exists(newpath): + os.chmod(newpath, 0666) + os.unlink(newpath) + shutil.copy2(path, newpath) + # set mtimes to be identical + os.utime(newpath, (modified, modified)) + backup = staticmethod(backup) + + +def newCardOrderLabels(): + return { + 0: _("Show new cards in random order"), + 1: _("Show new cards in order they were added"), + } + +def newCardSchedulingLabels(): + return { + 0: _("Spread new cards out through reviews"), + 1: _("Show new cards after all other cards"), + } diff --git a/anki/errors.py b/anki/errors.py new file mode 100644 index 000000000..ce496c76e --- /dev/null +++ b/anki/errors.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Errors +============================== +""" +__docformat__ = 'restructuredtext' + +class Error(Exception): + def __init__(self, message="", **data): + self.data = data + self.message = message + def __str__(self): + m = self.message + if self.data: + m += ": %s" % repr(self.data) + return m + +class DeckAccessError(Error): + "The deck is empty." + pass + +class DeckWrongFormatError(Error): + "A file to import is in the wrong format." + pass + +class DuplicateCardError(Error): + "Attempted to add a card with the same question." + pass + +class ImportFileError(Error): + "Unable to load file to import from." + pass + +class ImportFormatError(Error): + "Unable to determine pattern in text file." + pass + +class ImportEncodingError(Error): + "The file was not in utf-8." + pass + +class ExportFileError(Error): + "Unable to save file." + pass + +class SyncError(Error): + "A problem occurred during syncing." + pass + +# facts, models +class FactInvalidError(Error): + """A fact was invalid/not unique according to the model. +'field' defines the problem field. +'type' defines the type of error ('fieldEmpty', 'fieldNotUnique')""" + pass diff --git a/anki/exporting.py b/anki/exporting.py new file mode 100644 index 000000000..4f62d76a9 --- /dev/null +++ b/anki/exporting.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Exporting support +============================== +""" +__docformat__ = 'restructuredtext' + +import itertools, time +from operator import itemgetter +from anki import DeckStorage +from anki.cards import Card +from anki.sync import SyncClient, SyncServer +from anki.lang import _ +from anki.utils import findTag, parseTags, stripHTML, ids2str +from anki.db import * + +class Exporter(object): + def __init__(self, deck): + self.deck = deck + self.limitTags = [] + + def exportInto(self, path): + file = open(path, "wb") + self.doExport(file) + file.close() + + def escapeText(self, text): + "Escape newlines and tabs." + text = text.replace("\n", "
") + text = text.replace("\t", " " * 8) + return text + + def cardIds(self): + "Return all cards, limited by tags." + tlist = self.deck.tagsList() + cards = [id for (id, tags, pri) in tlist if self.hasTags(tags)] + self.count = len(cards) + return cards + + def hasTags(self, tags): + tags = parseTags(tags) + if not self.limitTags: + return True + for tag in self.limitTags: + if findTag(tag, tags): + return True + return False + +# FIXME: media support +class AnkiExporter(Exporter): + + key = _("Anki decks (*.anki)") + ext = ".anki" + + def __init__(self, deck): + Exporter.__init__(self, deck) + self.includeSchedulingInfo = False + + def exportInto(self, path): + self.newDeck = DeckStorage.Deck(path) + client = SyncClient(self.deck) + server = SyncServer(self.newDeck) + client.setServer(server) + client.localTime = self.deck.modified + client.remoteTime = 0 + self.deck.s.flush() + # set up a custom change list and sync + lsum = self.localSummary() + rsum = server.summary(0) + payload = client.genPayload((lsum, rsum)) + res = server.applyPayload(payload) + client.applyPayloadReply(res) + if not self.includeSchedulingInfo: + self.newDeck.s.statement(""" +update cards set +interval = 0.001, +lastInterval = 0, +due = created, +lastDue = 0, +factor = 2.5, +firstAnswered = 0, +reps = 0, +successive = 0, +averageTime = 0, +reviewTime = 0, +youngEase0 = 0, +youngEase1 = 0, +youngEase2 = 0, +youngEase3 = 0, +youngEase4 = 0, +matureEase0 = 0, +matureEase1 = 0, +matureEase2 = 0, +matureEase3 = 0, +matureEase4 = 0, +yesCount = 0, +noCount = 0, +spaceUntil = 0, +isDue = 1, +relativeDelay = 0, +type = 2, +combinedDue = created, +modified = :now +""", now=time.time()) + # need to save manually + self.newDeck.s.commit() + self.newDeck.close() + + def localSummary(self): + cardIds = self.cardIds() + cStrIds = ids2str(cardIds) + cards = self.deck.s.all(""" +select id, modified from cards +where id in %s""" % cStrIds) + facts = self.deck.s.all(""" +select facts.id, facts.modified from cards, facts where +facts.id = cards.factId and +cards.id in %s""" % cStrIds) + models = self.deck.s.all(""" +select models.id, models.modified from models, facts where +facts.modelId = models.id and +facts.id in %s""" % ids2str([f[0] for f in facts])) + media = self.deck.s.all(""" +select id, created from media""") + return { + # cards + "cards": cards, + "delcards": [], + # facts + "facts": facts, + "delfacts": [], + # models + "models": models, + "delmodels": [], + # media + "media": media, + "delmedia": [], + } + +class TextCardExporter(Exporter): + + key = _("Text files (*.txt)") + ext = ".txt" + + def __init__(self, deck): + Exporter.__init__(self, deck) + self.includeTags = False + + def doExport(self, file): + strids = ids2str(self.cardIds()) + cards = self.deck.s.all(""" +select cards.question, cards.answer, cards.id from cards +where cards.id in %s""" % strids) + if self.includeTags: + self.cardTags = dict(self.deck.s.all(""" +select cards.id, cards.tags || "," || facts.tags from cards, facts +where cards.factId = facts.id +and cards.id in %s""" % strids)) + out = u"\n".join(["%s\t%s%s" % (self.escapeText(c[0]), + self.escapeText(c[1]), + self.tags(c[2])) + for c in cards]) + if out: + out += "\n" + file.write(out.encode("utf-8")) + + def tags(self, id): + if self.includeTags: + return "\t" + ", ".join(parseTags(self.cardTags[id])) + return "" + +class TextFactExporter(Exporter): + + key = _("Text files (*.txt)") + ext = ".txt" + + def __init__(self, deck): + Exporter.__init__(self, deck) + self.includeTags = False + + def doExport(self, file): + cardIds = self.cardIds() + facts = self.deck.s.all(""" +select factId, value from fields +where +factId in +(select distinct facts.id from facts, cards +where facts.id = cards.factId +and cards.id in %s) +order by factId, ordinal""" % ids2str(cardIds)) + txt = "" + if self.includeTags: + self.factTags = dict(self.deck.s.all( + "select id, tags from facts where id in %s" % + ids2str([fact[0] for fact in facts]))) + out = ["\t".join([self.escapeText(x[1]) for x in ret[1]]) + + self.tags(ret[0]) + for ret in (itertools.groupby(facts, itemgetter(0)))] + self.count = len(out) + out = "\n".join(out) + file.write(out.encode("utf-8")) + + def tags(self, id): + if self.includeTags: + return "\t" + self.factTags[id] + return "" + +# Export modules +########################################################################## + +def exporters(): + return ( + (_("Anki deck (*.anki)"), AnkiExporter), + (_("Cards in tab-separated text file (*.txt)"), TextCardExporter), + (_("Facts in tab-separated text file (*.txt)"), TextFactExporter)) diff --git a/anki/facts.py b/anki/facts.py new file mode 100644 index 000000000..22c720586 --- /dev/null +++ b/anki/facts.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Facts +======== +""" +__docformat__ = 'restructuredtext' + +import time +from anki.db import * +from anki.errors import * +from anki.models import Model, FieldModel, fieldModelsTable +from anki.utils import genID +from anki.features import FeatureManager + +# Fields in a fact +########################################################################## + +fieldsTable = Table( + 'fields', metadata, + Column('id', Integer, primary_key=True), + Column('factId', Integer, ForeignKey("facts.id"), nullable=False), + Column('fieldModelId', Integer, ForeignKey("fieldModels.id"), + nullable=False), + Column('ordinal', Integer, nullable=False), + Column('value', UnicodeText, nullable=False)) + +class Field(object): + "A field in a fact." + + def __init__(self, fieldModel=None): + if fieldModel: + self.fieldModel = fieldModel + self.ordinal = fieldModel.ordinal + self.value = u"" + self.id = genID() + + def getName(self): + return self.fieldModel.name + name = property(getName) + +mapper(Field, fieldsTable, properties={ + 'fieldModel': relation(FieldModel) + }) + +# Facts: a set of fields and a model +########################################################################## +# mapped in cards.py + +factsTable = Table( + 'facts', metadata, + Column('id', Integer, primary_key=True), + Column('modelId', Integer, ForeignKey("models.id"), nullable=False), + Column('created', Float, nullable=False, default=time.time), + Column('modified', Float, nullable=False, default=time.time), + Column('tags', UnicodeText, nullable=False, default=u""), + # the following two fields are obsolete and now stored in cards table + Column('spaceUntil', Float, nullable=False, default=0), + Column('lastCardId', Integer, ForeignKey( + "cards.id", use_alter=True, name="lastCardIdfk"))) + +class Fact(object): + "A single fact. Fields exposed as dict interface." + + def __init__(self, model=None): + self.model = model + self.id = genID() + if model: + for fm in model.fieldModels: + self.fields.append(Field(fm)) + + def keys(self): + return [field.name for field in self.fields] + + def values(self): + return [field.value for field in self.fields] + + def __getitem__(self, key): + try: + return [f.value for f in self.fields if f.name == key][0] + except IndexError: + raise KeyError + + def __setitem__(self, key, value): + try: + [f for f in self.fields if f.name == key][0].value = value + except IndexError: + raise KeyError + + def get(self, key, default): + try: + return self[key] + except IndexError: + return default + + def css(self): + return "".join([f.fieldModel.css() for f in self.fields]) + + def assertValid(self): + "Raise an error if required fields are empty." + for field in self.fields: + if not self.fieldValid(field): + raise FactInvalidError(type="fieldEmpty", + field=field.name) + + def fieldValid(self, field): + return not (field.fieldModel.required and not field.value.strip()) + + def assertUnique(self, s): + "Raise an error if duplicate fields are found." + for field in self.fields: + if not self.fieldUnique(field, s): + raise FactInvalidError(type="fieldNotUnique", + field=field.name) + + def fieldUnique(self, field, s): + if not field.fieldModel.unique: + return True + req = ("select value from fields " + "where fieldModelId = :fmid and value = :val") + if field.id: + req += " and id != %s" % field.id + return not s.scalar(req, val=field.value, fmid=field.fieldModel.id) + + def onSubmit(self): + FeatureManager.run(self.model.features, "onSubmit", self) + + def onKeyPress(self, field, value): + FeatureManager.run(self.model.features, + "onKeyPress", self, field, value) + + def setModified(self, textChanged=False): + "Mark modified and update cards." + self.modified = time.time() + if textChanged: + for card in self.cards: + card.question = card.cardModel.renderQA(card, self, "question") + card.answer = card.cardModel.renderQA(card, self, "answer") + card.setModified() + +# Fact deletions +########################################################################## + +factsDeletedTable = Table( + 'factsDeleted', metadata, + Column('factId', Integer, ForeignKey("facts.id"), + nullable=False), + Column('deletedTime', Float, nullable=False)) diff --git a/anki/features/__init__.py b/anki/features/__init__.py new file mode 100644 index 000000000..745be81c8 --- /dev/null +++ b/anki/features/__init__.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Features - extensible features like auto-reading generation +=============================================================================== + +Features allow the deck to define specific features that are required, but +that can be resolved in real time. This includes things like automatic reading +generation, language-specific dictionary entries, etc. +""" + +from anki.lang import _ +from anki.errors import * +from anki.utils import findTag, parseTags + +class Feature(object): + + def __init__(self, tags=None, name="", description=""): + if not tags: + tags = [] + self.tags = tags + self.name = name + self.description = description + + def onSubmit(self, fact): + "Apply any last-minute modifications to FACT before addition." + pass + + def onKeyPress(self, fact): + "Apply any changes to fact as it's being edited for the first time." + pass + + def run(self, cmd, *args): + "Run CMD." + attr = getattr(self, cmd, None) + if attr: + attr(*args) + +class FeatureManager(object): + + features = {} + + def add(feature): + "Add a feature." + FeatureManager.features[feature.name] = feature + add = staticmethod(add) + + def run(tagstr, cmd, *args): + "Run CMD on all matching features in DLIST." + tags = parseTags(tagstr) + for (name, feature) in FeatureManager.features.items(): + for tag in tags: + if findTag(tag, feature.tags): + feature.run(cmd, *args) + break + run = staticmethod(run) + +# Add bundled features +import japanese +FeatureManager.add(japanese.FuriganaGenerator()) +import chinese +FeatureManager.add(chinese.CantoneseGenerator()) +FeatureManager.add(chinese.MandarinGenerator()) diff --git a/anki/features/chinese/README b/anki/features/chinese/README new file mode 100644 index 000000000..002023558 --- /dev/null +++ b/anki/features/chinese/README @@ -0,0 +1,4 @@ +Downloaded from http://www.unicode.org/Public/4.1.0/ucd/Unihan.zip + +Copyright: http://www.unicode.org/copyright.html + diff --git a/anki/features/chinese/__init__.py b/anki/features/chinese/__init__.py new file mode 100644 index 000000000..b06712dc8 --- /dev/null +++ b/anki/features/chinese/__init__.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +import sys, os, pickle +from anki.features import Feature +from anki.utils import findTag, parseTags, stripHTML +from anki.db import * + +class UnihanController(object): + + def __init__(self, target): + base = os.path.dirname(os.path.abspath(__file__)) + if not os.path.exists(base): + if sys.platform.startswith("darwin"): + base = os.path.dirname(sys.argv[0]) + else: + base = os.path.join(os.path.dirname(sys.argv[0]), + "features") + self.engine = create_engine("sqlite:///" + os.path.abspath( + os.path.join(base, "unihan.db")), + echo=False, strategy='threadlocal') + self.session = sessionmaker(bind=self.engine, + autoflush=False, + transactional=True) + self.type = target + + def reading(self, text): + text = stripHTML(text) + result = [] + s = SessionHelper(self.session()) + for c in text: + n = ord(c) + ret = s.scalar("select %s from unihan where id = :id" + % self.type, id=n) + if ret: + result.append(self.formatMatch(ret)) + return u" ".join(result) + + def formatMatch(self, match): + m = match.split(" ") + if len(m) == 1: + return m[0] + return "{%s}" % (",".join(m)) + +class ChineseGenerator(Feature): + + def __init__(self): + self.expressionField = "Expression" + self.readingField = "Reading" + + def lazyInit(self): + pass + + def onKeyPress(self, fact, field, value): + if findTag("Reading source", parseTags(field.fieldModel.features)): + dst = None + for field in fact.fields: + if findTag("Reading destination", + parseTags(field.fieldModel.features)): + dst = field + break + if not dst: + return + self.lazyInit() + reading = self.unihan.reading(value) + fact[dst.name] = reading + +class CantoneseGenerator(ChineseGenerator): + + def __init__(self): + ChineseGenerator.__init__(self) + self.tags = ["Cantonese"] + self.name = "Reading generation for Cantonese" + + def lazyInit(self): + if 'unihan' not in self.__dict__: + self.unihan = UnihanController("cantonese") + +class MandarinGenerator(ChineseGenerator): + + def __init__(self): + ChineseGenerator.__init__(self) + self.tags = ["Mandarin"] + self.name = "Reading generation for Mandarin" + + def lazyInit(self): + if 'unihan' not in self.__dict__: + self.unihan = UnihanController("mandarin") diff --git a/anki/features/chinese/save_unihan.py b/anki/features/chinese/save_unihan.py new file mode 100644 index 000000000..b94fa873a --- /dev/null +++ b/anki/features/chinese/save_unihan.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# read unihan.txt and save it as a db + +import psyco; psyco.full() + +from sqlalchemy import (Table, Integer, Float, Unicode, Column, MetaData, + ForeignKey, Boolean, String, Date, UniqueConstraint, + UnicodeText) +from sqlalchemy import (create_engine) +from sqlalchemy.orm import mapper, sessionmaker, relation, backref, \ + object_session as _object_session +from sqlalchemy.sql import select, text, and_ +from sqlalchemy.exceptions import DBAPIError +import re + +metadata = MetaData() + +unihanTable = Table( + 'unihan', metadata, + Column("id", Integer, primary_key=True), + Column("mandarin", UnicodeText), + Column("cantonese", UnicodeText), + Column("grade", Integer), + ) + +engine = create_engine("sqlite:///unihan.db", + echo=False, strategy='threadlocal') +session = sessionmaker(bind=engine, + autoflush=False, + transactional=True) +metadata.create_all(engine) + +s = session() + +# Convert codes to accents +########################################################################## +# code from Donald Chai + +accenttable = { + 'a' : [u'a', u'ā', u'á', u'ǎ', u'à', u'a'], + 'e' : [u'e', u'ē', u'é', u'ě', u'è', u'e'], + 'i' : [u'i', u'ī', u'í', u'ǐ', u'ì', u'i'], + 'o' : [u'o', u'ō', u'ó', u'ǒ', u'ò', u'o'], + 'u' : [u'u', u'ū', u'ú', u'ǔ', u'ù', u'u'], + 'v' : [u'ü', u'ǖ', u'ǘ', u'ǚ', u'ǜ', u'ü'], +} +def convert(word): + '''Converts a pinyin word to unicode''' + word = word.lower() + orig = word + # convert ü to v for now to make life easier + word = re.sub(u'\xfc|\xc3\xbc', 'v', word) + # extract fields + mo = re.match('([qwrtypsdfghjklzxcbnm]*)([aeiouv]*)(\D*)(\d?)', word) + init = mo.group(1) + vowel = mo.group(2) + final = mo.group(3) + tone = mo.group(4) + # do nothing if no vowel or tone + if vowel=='' or tone=='': + return orig + tone = int(tone) + if len(vowel)==1: + vowel = accenttable[vowel][tone] + elif vowel[-2]=='i' or vowel[-2]=='u': + # put over last + vowel = vowel[:-1] + accenttable[vowel[-1]][tone] + else: + # put over second to last + vowel = vowel[:-2] + accenttable[vowel[-2]][tone] + vowel[-1] + return init+vowel+final + +########################################################################## + +kanji = {} +import codecs +for line in codecs.open("Unihan.txt", encoding="utf-8"): + try: + (u, f, v) = line.strip().split("\t") + except ValueError: + continue + if not u.startswith("U+"): + continue + n = int(u.replace("U+",""), 16) + if not n in kanji: + kanji[n] = {} + if f == "kMandarin": + kanji[n]['mandarin'] = " ".join([convert(w) for w in v.split()]) + elif f == "kCantonese": + kanji[n]['cantonese'] = v + elif f == "kGradeLevel": + kanji[n]['grade'] = int(v) + +dict = [{'id': k, + 'mandarin': v.get('mandarin'), + 'cantonese': v.get('cantonese'), + 'grade': v.get('grade') } for (k,v) in kanji.items() + if v.get('mandarin') or v.get('cantonese') or v.get('grade')] +s.execute(unihanTable.insert(), dict) + +s.commit() diff --git a/anki/features/japanese.py b/anki/features/japanese.py new file mode 100644 index 000000000..23d10c5ff --- /dev/null +++ b/anki/features/japanese.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +import sys, os +from anki.features import Feature +from anki.utils import findTag, parseTags, stripHTML + +class KakasiController(object): + def __init__(self): + # add our pre-packaged kakasi to the path + if sys.platform == "win32": + dir = os.path.dirname(sys.argv[0]) + os.environ['PATH'] += ";" + dir + "\\kakasi\\bin" + shareDir = dir + "\\kakasi\\share\\kakasi\\" + os.environ['ITAIJIDICT'] = shareDir + "\\itaijidict" + os.environ['KANWADICT'] = shareDir + "\\kanwadict" + elif sys.platform.startswith("darwin"): + dir = os.path.dirname(os.path.abspath(__file__)) + dir = os.path.abspath(dir + "/../../../../..") + import platform + # don't add kakasi to the path on powerpc, it's buggy + # and loop until this works, since processor() is buggy + while 1: + try: + proc = platform.processor() + except IOError: + proc = None + if proc: + break + if proc != "powerpc": + os.environ['PATH'] += ":" + dir + "/kakasi" + os.environ['ITAIJIDICT'] = dir + "/kakasi/itaijidict" + os.environ['KANWADICT'] = dir + "/kakasi/kanwadict" + self._open = False + + # we don't launch kakasi until it's actually required + def lazyopen(self): + if not self._open: + if not self.available(): + from errno import ENOENT + raise OSError(ENOENT, 'Kakasi not available') + # don't convert kana to hiragana + (self.kin, self.kout) = os.popen2("kakasi -isjis -osjis -u -JH -p") + self._open = True + + def close(self): + if self._open: + self.kin.close() + self.kout.close() + + def toFurigana(self, kanji): + self.lazyopen() + kanji = self.formatForKakasi(kanji) + try: + self.kin.write(kanji.encode("sjis", "ignore")+'\n') + self.kin.flush() + return unicode(self.kout.readline().rstrip('\n'), "sjis") + except IOError: + return u"" + + def formatForKakasi(self, text): + "Strip characters kakasi can't handle." + # kakasi is line based + text = text.replace("\n", " ") + text = text.replace(u'\uff5e', "~") + text = text.replace("
", "---newline---") + text = text.replace("
", "---newline---") + text = stripHTML(text) + text = text.replace("---newline---", "
") + return text + + def available(self): + if sys.platform in ("win32",): + executable = 'kakasi.exe' + else: + executable = 'kakasi' + for d in os.environ['PATH'].split(os.pathsep): + if os.path.exists(os.path.join(d, executable)): + return True + return False + +class FuriganaGenerator(Feature): + + def __init__(self): + self.tags = ["Japanese"] + self.name = "Furigana generation based on kakasi." + self.kakasi = KakasiController() + if not self.kakasi.available(): + self.kakasi = None + + def onKeyPress(self, fact, field, value): + if self.kakasi and findTag("Reading source", + parseTags(field.fieldModel.features)): + reading = self.kakasi.toFurigana(value) + dst = None + for field in fact.fields: + if findTag("Reading destination", parseTags( + field.fieldModel.features)): + dst = field + break + if dst: + if self.kakasi.formatForKakasi(value) != reading: + fact[dst.name] = reading + else: + fact[dst.name] = u"" diff --git a/anki/fonts.py b/anki/fonts.py new file mode 100644 index 000000000..1bff954e9 --- /dev/null +++ b/anki/fonts.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Fonts - mapping to/from platform-specific fonts +============================================================== +""" + +import sys + +# set this to 'all', to get all fonts in a list +policy="platform" + +mapping = [ + [u"Mincho", u"MS Mincho", "win32"], + [u"Mincho", u"MS 明朝", "win32"], + [u"Mincho", u"ヒラギノ明朝 Pro W3", "mac"], + [u"Mincho", u"Kochi Mincho", "linux"], + [u"Mincho", u"東風明朝", "linux"], + ] + +def platform(): + if sys.platform == "win32": + return "win32" + elif sys.platform.startswith("darwin"): + return "mac" + else: + return "linux" + +def toCanonicalFont(family): + "Turn a platform-specific family into a canonical one." + for (s, p, type) in mapping: + if family == p: + return s + return family + +def toPlatformFont(family): + "Turn a canonical font into a platform-specific one." + if policy == "all": + return allFonts(family) + ltype = platform() + for (s, p, type) in mapping: + if family == s and type == ltype: + return p + return family + +def substitutions(): + "Return a tuple mapping canonical fonts to platform ones." + type = platform() + return [(s, p) for (s, p, t) in mapping if t == type] + +def allFonts(family): + ret = ", ".join([p for (s, p, t) in mapping if s == family]) + return ret or family diff --git a/anki/graphs.py b/anki/graphs.py new file mode 100644 index 000000000..a56c13e6f --- /dev/null +++ b/anki/graphs.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Graphs of deck statistics +============================== +""" +__docformat__ = 'restructuredtext' + +import os, sys, time +import anki.stats + +# support frozen distribs +if getattr(sys, "frozen", None): + os.environ['MATPLOTLIBDATA'] = os.path.join( + os.path.dirname(sys.argv[0]), + "matplotlibdata") + +try: + from matplotlib.figure import Figure +except UnicodeEncodeError: + # haven't tracked down the cause of this yet, but reloading fixes it + try: + from matplotlib.figure import Figure + except ImportError: + pass +except ImportError: + pass + +def graphsAvailable(): + return 'matplotlib' in sys.modules + +class DeckGraphs(object): + + def __init__(self, deck, width=8, height=3, dpi=75): + self.deck = deck + self.stats = None + self.width = width + self.height = height + self.dpi = dpi + + def calcStats (self): + if not self.stats: + days = {} + months = {} + next = {} + lowestInDay = 0 + now = list(time.gmtime(time.time())) + now[3] = 0; now[4] = 0 + self.startOfDay = time.mktime(now) + all = self.deck.s.all(""" +select interval, combinedDue +from cards where reps > 0 and priority != 0""") + for (interval, due) in all: + day=int(round(interval)) + days[day] = days.get(day, 0) + 1 + indays = int(round((due - self.startOfDay) + / 86400.0)) + next[indays] = next.get(indays, 0) + 1 + if indays < lowestInDay: + lowestInDay = indays + self.stats = {} + self.stats['next'] = next + self.stats['days'] = days + self.stats['months'] = months + self.stats['lowestInDay'] = lowestInDay + + def nextDue(self, days=30): + self.calcStats() + fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) + graph = fig.add_subplot(111) + dayslist = self.stats['next'] + self.addMissing(dayslist, self.stats['lowestInDay'], days) + (x, y) = self.unzip(dayslist.items()) + self.filledGraph(graph, days, x, y, "#4444ff") + graph.set_ylabel(_("Cards")) + graph.set_xlabel(_("Days")) + graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days) + return fig + + def cumulativeDue(self, days=30): + self.calcStats() + fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) + graph = fig.add_subplot(111) + (x, y) = self.unzip(self.stats['next'].items()) + count=0 + y = list(y) + for i in range(len(x)): + count = count + y[i] + if i == 0: + continue + y[i] = count + if x[i] > days: + break + x = list(x); x.append(99999) + y.append(count) + self.filledGraph(graph, days, x, y, "#aaaaff") + graph.set_ylabel(_("Cards")) + graph.set_xlabel(_("Days")) + graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days) + graph.set_ylim(ymax=count+100) + return fig + + def intervalPeriod(self, days=30): + self.calcStats() + fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) + ints = self.stats['days'] + self.addMissing(ints, 0, days) + intervals = self.unzip(ints.items()) + graph = fig.add_subplot(111) + self.filledGraph(graph, days, intervals[0], intervals[1], "#aaffaa") + graph.set_ylabel(_("Cards")) + graph.set_xlabel(_("Days")) + graph.set_xlim(xmin=0, xmax=days) + return fig + + def addedRecently(self, numdays=30, attr='created'): + days = {} + fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) + limit = time.time() - numdays * 86400 + res = self.deck.s.column0("select %s from cards where %s >= %f" % + (attr, attr, limit)) + for r in res: + d = (r - self.startOfDay) / 86400.0 + days[round(d)] = days.get(round(d), 0) + 1 + self.addMissing(days, -numdays, 0) + graph = fig.add_subplot(111) + intervals = self.unzip(days.items()) + if attr == 'created': + colour = "#ffaaaa" + else: + colour = "#ffcccc" + self.filledGraph(graph, numdays, intervals[0], intervals[1], colour) + graph.set_ylabel(_("Cards")) + graph.set_xlabel(_("Day")) + graph.set_xlim(xmin=-numdays, xmax=0) + return fig + + def addMissing(self, dict, min, max): + for i in range(min, max+1): + if not i in dict: + dict[i] = 0 + + def unzip(self, tuples, fillFix=True): + tuples.sort(cmp=lambda x,y: cmp(x[0], y[0])) + new = zip(*tuples) + return new + + def filledGraph(self, graph, days, x=(), y=(), c="b"): + x = list(x) + y = list(y) + lowest = 99999 + highest = -lowest + for i in range(len(x)): + if x[i] < lowest: + lowest = x[i] + if x[i] > highest: + highest = x[i] + # ensure the filled area reaches the bottom + x.insert(0, lowest - 1) + y.insert(0, 0) + x.append(highest + 1) + y.append(0) + # plot + graph.fill(x, y, c) + if days < 180: + graph.bar(x, y, width=0) + lw=3 + else: + lw=1 + graph.plot(x, y, "k", lw=lw) + graph.grid(True) + graph.set_ylim(ymin=0, ymax=max(2, graph.get_ylim()[1])) + + def easeBars(self): + fig = Figure(figsize=(3, 3), dpi=self.dpi) + graph = fig.add_subplot(111) + types = ("new", "young", "mature") + enum = 5 + offset = 0 + arrsize = 17 + arr = [0] * arrsize + n = 0 + colours = ["#ff7777", "#77ffff", "#7777ff"] + bars = [] + gs = anki.stats.globalStats(self.deck.s) + for type in types: + total = (getattr(gs, type + "Ease0") + + getattr(gs, type + "Ease1") + + getattr(gs, type + "Ease2") + + getattr(gs, type + "Ease3") + + getattr(gs, type + "Ease4")) + for e in range(enum): + try: + arr[e+offset] = (getattr(gs, type + "Ease%d" % e) + / float(total)) * 100 + 1 + except ZeroDivisionError: + arr[e+offset] = 0 + bars.append(graph.bar(range(arrsize), arr, width=1.0, + color=colours[n], align='center')) + arr = [0] * arrsize + offset += 6 + n += 1 + graph.set_ylabel("%") + x = ([""] + [str(n) for n in range(enum)]) * 3 + del x[0] + graph.legend([p[0] for p in bars], (_("New cards"), + _("Young cards"), + _("Mature cards")), + 'upper left') + graph.set_ylim(ymax=100) + graph.set_xticks(range(arrsize)) + graph.set_xticklabels(x) + graph.grid(True) + return fig diff --git a/anki/history.py b/anki/history.py new file mode 100644 index 000000000..44230599b --- /dev/null +++ b/anki/history.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +History - keeping a record of all reviews +========================================== +""" +__docformat__ = 'restructuredtext' + +import time +from anki.db import * + +reviewHistoryTable = Table( + 'reviewHistory', metadata, + Column('id', Integer, primary_key=True), + Column('cardId', Integer, ForeignKey("cards.id")), + Column('time', Float, nullable=False, default=time.time), + Column('lastInterval', Float, nullable=False), + Column('nextInterval', Float, nullable=False), + Column('ease', Integer, nullable=False), + Column('delay', Float, nullable=False), + Column('lastFactor', Float, nullable=False), + Column('nextFactor', Float, nullable=False), + Column('reps', Float, nullable=False), + Column('thinkingTime', Float, nullable=False), + Column('yesCount', Float, nullable=False), + Column('noCount', Float, nullable=False)) + +class CardHistoryEntry(object): + "Create after rescheduling card." + + def __init__(self, card=None, ease=None, delay=None): + if not card: + return + self.cardId = card.id + self.lastInterval = card.lastInterval + self.nextInterval = card.interval + self.lastFactor = card.lastFactor + self.nextFactor = card.factor + self.reps = card.reps + self.yesCount = card.yesCount + self.noCount = card.noCount + self.ease = ease + self.delay = delay + self.thinkingTime = card.thinkingTime() + + def writeSQL(self, s): + s.statement(""" +insert into reviewHistory +(cardId, lastInterval, nextInterval, ease, delay, lastFactor, +nextFactor, reps, thinkingTime, yesCount, noCount, time) +values ( +:cardId, :lastInterval, :nextInterval, :ease, :delay, +:lastFactor, :nextFactor, :reps, :thinkingTime, :yesCount, :noCount, +:time)""", + cardId=self.cardId, + lastInterval=self.lastInterval, + nextInterval=self.nextInterval, + ease=self.ease, + delay=self.delay, + lastFactor=self.lastFactor, + nextFactor=self.nextFactor, + reps=self.reps, + thinkingTime=self.thinkingTime, + yesCount=self.yesCount, + noCount=self.noCount, + time=time.time()) + +mapper(CardHistoryEntry, reviewHistoryTable) diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py new file mode 100644 index 000000000..fbb619d40 --- /dev/null +++ b/anki/importing/__init__.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Importing support +============================== + +To import, a mapping is created of the form: [FieldModel, ...]. The mapping +may be extended by calling code if a file has more fields. To ignore a +particular FieldModel, replace it with None. The same field model should not +occur more than once.""" + +__docformat__ = 'restructuredtext' + +import time +from anki.cards import cardsTable +from anki.facts import factsTable, fieldsTable +from anki.lang import _ +from anki.utils import genID +from anki.errors import * + +# Base importer +########################################################################## + +class ForeignCard(object): + "An temporary object storing fields and attributes." + def __init__(self): + self.fields = [] + self.tags = u"" + +class Importer(object): + + needMapper = True + tagDuplicates = False + multipleCardsAllowed = True + + def __init__(self, deck, file): + self.file = file + self._model = deck.currentModel + self._mapping = None + self.log = [] + self.deck = deck + self.total = 0 + self.tagsToAdd = u"" + + def doImport(self): + "Import." + c = self.foreignCards() + self.importCards(c) + if c: + self.deck.setModified() + + def fields(self): + "The number of fields." + return 0 + + def foreignCards(self): + "Return a list of foreign cards for importing." + assert 0 + + def resetMapping(self): + "Reset mapping to default." + numFields = self.fields() + m = [] + [m.append(f) for f in self.model.fieldModels if f.required] + [m.append(f) for f in self.model.fieldModels if not f.required] + rem = max(0, self.fields() - len(m)) + m += [None] * rem + del m[numFields:] + self._mapping = m + + def getMapping(self): + if not self._mapping: + self.resetMapping() + return self._mapping + + def setMapping(self, mapping): + self._mapping = mapping + + mapping = property(getMapping, setMapping) + + def getModel(self): + return self._model + + def setModel(self, model): + self._model = model + # update the mapping for the new model + self._mapping = None + self.getMapping() + + model = property(getModel, setModel) + + def importCards(self, cards): + "Convert each card into a fact, apply attributes and add to deck." + # ensure all unique and required fields are mapped + for fm in self.model.fieldModels: + if fm.required or fm.unique: + if fm not in self.mapping: + raise ImportFormatError( + type="missingRequiredUnique", + info=_("Missing required/unique field '%(field)s'") % + {'field': fm.name}) + active = 0 + for cm in self.model.cardModels: + if cm.active: active += 1 + if active > 1 and not self.multipleCardsAllowed: + raise ImportFormatError(type="tooManyCards", + info=_(""" +The current importer only supports a single active card model. Please disable +all but one card model.""")) + # strip invalid cards + cards = self.stripInvalid(cards) + cards = self.stripOrTagDupes(cards) + if cards: + self.addCards(cards) + + def addCards(self, cards): + "Add facts in bulk from foreign cards." + # add facts + factIds = [genID() for n in range(len(cards))] + self.deck.s.execute(factsTable.insert(), + [{'modelId': self.model.id, + 'tags': self.tagsToAdd, + 'id': factIds[n]} for n in range(len(cards))]) + self.deck.s.execute(""" +delete from factsDeleted +where factId in (%s)""" % ",".join([str(s) for s in factIds])) + # add all the fields + for fm in self.model.fieldModels: + try: + index = self.mapping.index(fm) + except ValueError: + index = None + data = [{'factId': factIds[m], + 'fieldModelId': fm.id, + 'ordinal': fm.ordinal, + 'id': genID(), + 'value': (index is not None and + cards[m].fields[index] or u"")} + for m in range(len(cards))] + self.deck.s.execute(fieldsTable.insert(), + data) + # and cards + now = time.time() + for cm in self.model.cardModels: + self._now = now + if cm.active: + data = [self.addMeta({ + 'id': genID(), + 'factId': factIds[m], + 'cardModelId': cm.id, + 'ordinal': cm.ordinal, + 'question': cm.renderQASQL('q', factIds[m]), + 'answer': cm.renderQASQL('a', factIds[m]), + 'type': 2},cards[m]) for m in range(len(cards))] + self.deck.s.execute(cardsTable.insert(), + data) + self.total = len(factIds) + + def addMeta(self, data, card): + "Add any scheduling metadata to cards" + if 'fields' in card.__dict__: + del card.fields + data['created'] = self._now + data['modified'] = self._now + data['due'] = self._now + self._now += .00001 + data.update(card.__dict__) + return data + + def stripInvalid(self, cards): + return [c for c in cards if self.cardIsValid(c)] + + def cardIsValid(self, card): + fieldNum = len(card.fields) + for n in range(len(self.mapping)): + if self.mapping[n] and self.mapping[n].required: + if fieldNum <= n or not card.fields[n].strip(): + self.log.append("Fact is missing field '%s': %s" % + (self.mapping[n].name, + ", ".join(card.fields))) + return False + return True + + def stripOrTagDupes(self, cards): + # build a cache of items + self.uniqueCache = {} + for field in self.mapping: + if field and field.unique: + self.uniqueCache[field.id] = self.getUniqueCache(field) + return [c for c in cards if self.cardIsUnique(c)] + + def getUniqueCache(self, field): + "Return a dict with all fields, to test for uniqueness." + return dict(self.deck.s.all( + "select value, 1 from fields where fieldModelId = :fmid", + fmid=field.id)) + + def cardIsUnique(self, card): + fields = [] + for n in range(len(self.mapping)): + if self.mapping[n] and self.mapping[n].unique: + if card.fields[n] in self.uniqueCache[self.mapping[n].id]: + if not self.tagDuplicates: + self.log.append("Fact has duplicate '%s': %s" % + (self.mapping[n].name, + ", ".join(card.fields))) + return False + fields.append(self.mapping[n].name) + else: + self.uniqueCache[self.mapping[n].id][card.fields[n]] = 1 + if fields: + card.tags += u"Import: duplicate, Duplicate: " + ( + "+".join(fields)) + return True + +# Export modules +########################################################################## + +from anki.importing.csv import TextImporter +from anki.importing.anki10 import Anki10Importer +from anki.importing.mnemosyne10 import Mnemosyne10Importer +from anki.importing.wcu import WCUImporter + +Importers = ( + (_("TAB/semicolon-separated file (*.*)"), TextImporter), + (_("Anki 1.0 deck (*.anki)"), Anki10Importer), + (_("Mnemosyne 1.0 deck (*.mem)"), Mnemosyne10Importer), + (_("CueCard deck (*.wcu)"), WCUImporter), + ) diff --git a/anki/importing/anki03.py b/anki/importing/anki03.py new file mode 100644 index 000000000..9a3c5ad16 --- /dev/null +++ b/anki/importing/anki03.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Importing Anki v0.3 decks +========================== +""" +__docformat__ = 'restructuredtext' + +import cPickle, cStringIO, os, datetime, types, sys +from anki.models import Model, FieldModel, CardModel +from anki.facts import Fact, factsTable, fieldsTable +from anki.cards import cardsTable +from anki.stats import * +from anki.features import FeatureManager +from anki.importing import Importer +from anki.utils import genID + +def transformClasses(m, c): + "Map objects into dummy classes" + class EmptyClass(object): + pass + class EmptyList(list): + pass + class EmptyDict(dict): + pass + if c == "Fact": + return EmptyDict + elif c in ("CardModels", "Deck", "Facts", "Fields", "Models"): + return EmptyList + return EmptyClass + +def load(path): + "Load a deck from PATH." + if isinstance(path, types.UnicodeType): + path = path.encode(sys.getfilesystemencoding()) + file = open(path, "rb") + try: + try: + data = file.read() + except (IOError, OSError): + raise ImportError + finally: + file.close() + unpickler = cPickle.Unpickler(cStringIO.StringIO(data)) + unpickler.find_global = transformClasses + deck = unpickler.load() + deck.path = unicode(os.path.abspath(path), sys.getfilesystemencoding()) + return deck + +# we need to upgrade the deck before converting +def maybeUpgrade(deck): + # change old hardInterval from 1 day to 8-12 hours + if list(deck.sched.hardInterval) == [1.0, 1.0]: + deck.sched.hardInterval = [0.3333, 0.5] + # add 'medium priority' support + if not hasattr(deck.sched, 'medPriority'): + deck.sched.medPriority = [] + # add delay2 + if not hasattr(deck.sched, "delay2"): + deck.sched.delay2 = 28800 + # add collapsing + if not hasattr(deck.sched, "collapse"): + deck.sched.collapse = 18000 + # card related + # - add 'total' attribute + for card in deck: + card.__dict__['total'] = ( + card.stats['new']['yes'] + + card.stats['new']['no'] + + card.stats['young']['yes'] + + card.stats['young']['no'] + + card.stats['old']['yes'] + + card.stats['old']['no']) + +class Anki03Importer(Importer): + + needMapper = False + + def doImport(self): + oldDeck = load(self.file) + maybeUpgrade(oldDeck) + # mappings for old->new ids + cardModels = {} + fieldModels = {} + # start with the models + s = self.deck.s + deck = self.deck + import types + def uni(txt): + if txt is None: + return txt + if not isinstance(txt, types.UnicodeType): + txt = unicode(txt, "utf-8") + return txt + for oldModel in oldDeck.facts.models: + model = Model(uni(oldModel.name), uni(oldModel.description)) + model.id = oldModel.id + model.tags = u", ".join(oldModel.tags) + model.features = u", ".join(oldModel.decorators) + model.created = oldModel.created + model.modified = oldModel.modified + deck.newCardOrder = min(1, oldModel.position) + deck.addModel(model) + # fields + for oldField in oldModel.fields: + fieldModel = FieldModel(uni(oldField.name), + uni(oldField.description), + oldField.name in oldModel.required, + oldField.name in oldModel.unique) + fieldModel.features = u", ".join(oldField.features) + fieldModel.quizFontFamily = uni(oldField.display['quiz']['fontFamily']) + fieldModel.quizFontSize = oldField.display['quiz']['fontSize'] + fieldModel.quizFontColour = uni(oldField.display['quiz']['fontColour']) + fieldModel.editFontFamily = uni(oldField.display['edit']['fontFamily']) + fieldModel.editFontSize = oldField.display['edit']['fontSize'] + fieldModel.id = oldField.id + model.addFieldModel(fieldModel) + s.flush() # we need the id + fieldModels[id(oldField)] = fieldModel + # card models + for oldCard in oldModel.allcards: + cardModel = CardModel(uni(oldCard.name), + uni(oldCard.description), + uni(oldCard.qformat), + uni(oldCard.aformat)) + cardModel.active = oldCard in oldModel.cards + cardModel.questionInAnswer = oldCard.questionInAnswer + cardModel.id = oldCard.id + model.spacing = 0.25 + cardModel.questionFontFamily = uni(oldCard.display['question']['fontFamily']) + cardModel.questionFontSize = oldCard.display['question']['fontSize'] + cardModel.questionFontColour = uni(oldCard.display['question']['fontColour']) + cardModel.questionAlign = oldCard.display['question']['align'] + cardModel.answerFontFamily = uni(oldCard.display['answer']['fontFamily']) + cardModel.answerFontSize = oldCard.display['answer']['fontSize'] + cardModel.answerFontColour = uni(oldCard.display['answer']['fontColour']) + cardModel.answerAlign = oldCard.display['answer']['align'] + cardModel.lastFontFamily = uni(oldCard.display['last']['fontFamily']) + cardModel.lastFontSize = oldCard.display['last']['fontSize'] + cardModel.lastFontColour = uni(oldCard.display['last']['fontColour']) + cardModel.editQuestionFontFamily = ( + uni(oldCard.display['editQuestion']['fontFamily'])) + cardModel.editQuestionFontSize = ( + oldCard.display['editQuestion']['fontSize']) + cardModel.editAnswerFontFamily = ( + uni(oldCard.display['editAnswer']['fontFamily'])) + cardModel.editAnswerFontSize = ( + oldCard.display['editAnswer']['fontSize']) + model.addCardModel(cardModel) + s.flush() # we need the id + cardModels[id(oldCard)] = cardModel + # facts + def getSpace(lastCard, lastAnswered): + if not lastCard: + return 0 + return lastAnswered + lastCard.delay + def getLastCardId(fact): + if not fact.lastCard: + return None + ret = [c.id for c in fact.cards if c.model.id == fact.lastCard.id] + if ret: + return ret[0] + d = [{'id': f.id, + 'modelId': f.model.id, + 'created': f.created, + 'modified': f.modified, + 'tags': u",".join(f.tags), + 'spaceUntil': getSpace(f.lastCard, f.lastAnswered), + 'lastCardId': getLastCardId(f) + } for f in oldDeck.facts] + if d: + s.execute(factsTable.insert(), d) + self.total = len(oldDeck.facts) + # fields in facts + toAdd = [] + for oldFact in oldDeck.facts: + for field in oldFact.model.fields: + toAdd.append({'factId': oldFact.id, + 'id': genID(), + 'fieldModelId': fieldModels[id(field)].id, + 'ordinal': fieldModels[id(field)].ordinal, + 'value': uni(oldFact.get(field.name, u""))}) + if toAdd: + s.execute(fieldsTable.insert(), toAdd) + # cards + class FakeObj(object): + pass + fake = FakeObj() + fake.fact = FakeObj() + fake.fact.model = FakeObj() + fake.cardModel = FakeObj() + def renderQA(c, type): + fake.tags = u", ".join(c.tags) + fake.fact.tags = u", ".join(c.fact.tags) + fake.fact.model.tags = u", ".join(c.fact.model.tags) + fake.cardModel.name = c.model.name + return cardModels[id(c.model)].renderQA(fake, c.fact, type) + d = [{'id': c.id, + 'created': c.created, + 'modified': c.modified, + 'factId': c.fact.id, + 'ordinal': cardModels[id(c.model)].ordinal, + 'cardModelId': cardModels[id(c.model)].id, + 'tags': u", ".join(c.tags), + 'factor': 2.5, + 'firstAnswered': c.firstAnswered, + 'interval': c.interval, + 'lastInterval': c.lastInterval, + 'modified': c.modified, + 'due': c.nextTime, + 'lastDue': c.lastTime, + 'reps': c.total, + 'question': renderQA(c, 'question'), + 'answer': renderQA(c, 'answer'), + 'averageTime': c.stats['averageTime'], + 'reviewTime': c.stats['totalTime'], + 'yesCount': (c.stats['new']['yes'] + + c.stats['young']['yes'] + + c.stats['old']['yes']), + 'noCount': (c.stats['new']['no'] + + c.stats['young']['no'] + + c.stats['old']['no']), + 'successive': c.stats['successivelyCorrect']} + for c in oldDeck] + if d: + s.execute(cardsTable.insert(), d) + # scheduler + deck.description = uni(oldDeck.description) + deck.created = oldDeck.created + deck.maxScheduleTime = oldDeck.sched.maxScheduleTime + deck.hardIntervalMin = oldDeck.sched.hardInterval[0] + deck.hardIntervalMax = oldDeck.sched.hardInterval[1] + deck.midIntervalMin = oldDeck.sched.midInterval[0] + deck.midIntervalMax = oldDeck.sched.midInterval[1] + deck.easyIntervalMin = oldDeck.sched.easyInterval[0] + deck.easyIntervalMax = oldDeck.sched.easyInterval[1] + deck.delay0 = oldDeck.sched.delay0 + deck.delay1 = oldDeck.sched.delay1 + deck.delay2 = oldDeck.sched.delay2 + deck.collapseTime = 3600 # oldDeck.sched.collapse + deck.highPriority = u", ".join(oldDeck.sched.highPriority) + deck.medPriority = u", ".join(oldDeck.sched.medPriority) + deck.lowPriority = u", ".join(oldDeck.sched.lowPriority) + deck.suspended = u", ".join(oldDeck.sched.suspendedTags) + # scheduler global stats + stats = Stats() + stats.create(deck.s, 0, datetime.date.today()) + stats.day = datetime.date.fromtimestamp(oldDeck.created) + stats.averageTime = oldDeck.sched.globalStats['averageTime'] + stats.reviewTime = oldDeck.sched.globalStats['totalTime'] + stats.distractedTime = 0 + stats.distractedReps = 0 + stats.newEase0 = oldDeck.sched.easeStats.get('new', {}).get(0, 0) + stats.newEase1 = oldDeck.sched.easeStats.get('new', {}).get(1, 0) + stats.newEase2 = oldDeck.sched.easeStats.get('new', {}).get(2, 0) + stats.newEase3 = oldDeck.sched.easeStats.get('new', {}).get(3, 0) + stats.newEase4 = oldDeck.sched.easeStats.get('new', {}).get(4, 0) + stats.youngEase0 = oldDeck.sched.easeStats.get('young', {}).get(0, 0) + stats.youngEase1 = oldDeck.sched.easeStats.get('young', {}).get(1, 0) + stats.youngEase2 = oldDeck.sched.easeStats.get('young', {}).get(2, 0) + stats.youngEase3 = oldDeck.sched.easeStats.get('young', {}).get(3, 0) + stats.youngEase4 = oldDeck.sched.easeStats.get('young', {}).get(4, 0) + stats.matureEase0 = oldDeck.sched.easeStats.get('old', {}).get(0, 0) + stats.matureEase1 = oldDeck.sched.easeStats.get('old', {}).get(1, 0) + stats.matureEase2 = oldDeck.sched.easeStats.get('old', {}).get(2, 0) + stats.matureEase3 = oldDeck.sched.easeStats.get('old', {}).get(3, 0) + stats.matureEase4 = oldDeck.sched.easeStats.get('old', {}).get(4, 0) + yesCount = (oldDeck.sched.globalStats['new']['yes'] + + oldDeck.sched.globalStats['young']['yes'] + + oldDeck.sched.globalStats['old']['yes']) + noCount = (oldDeck.sched.globalStats['new']['no'] + + oldDeck.sched.globalStats['young']['no'] + + oldDeck.sched.globalStats['old']['no']) + stats.reps = yesCount + noCount + stats.toDB(deck.s) + # ignore daily stats & history, they make no sense on new version + s.flush() + deck.updateAllPriorities() + # save without updating mod time + deck.modified = oldDeck.modified + deck.lastLoaded = deck.modified + deck.s.commit() + deck.save() diff --git a/anki/importing/anki10.py b/anki/importing/anki10.py new file mode 100644 index 000000000..76949561a --- /dev/null +++ b/anki/importing/anki10.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Importing Anki 0.9+ decks +========================== +""" +__docformat__ = 'restructuredtext' + +from anki import DeckStorage +from anki.importing import Importer +from anki.sync import SyncClient, SyncServer + +class Anki10Importer(Importer): + + needMapper = False + + def doImport(self): + "Import." + src = DeckStorage.Deck(self.file) + client = SyncClient(self.deck) + server = SyncServer(src) + client.setServer(server) + # if there is a conflict, sync local -> src + client.localTime = self.deck.modified + client.remoteTime = 0 + src.s.execute("update facts set modified = 1") + src.s.execute("update models set modified = 1") + src.s.execute("update cards set modified = 1") + src.s.execute("update media set created = 1") + self.deck.s.flush() + # set up a custom change list and sync + lsum = client.summary(0) + self._clearDeleted(lsum) + rsum = server.summary(0) + self._clearDeleted(rsum) + payload = client.genPayload((lsum, rsum)) + # no need to add anything to src + payload['added-models'] = [] + payload['added-cards'] = [] + payload['added-facts'] = {'facts': [], 'fields': []} + assert payload['deleted-facts'] == [] + assert payload['deleted-cards'] == [] + assert payload['deleted-models'] == [] + res = server.applyPayload(payload) + client.applyPayloadReply(res) + # add tags + fids = [f[0] for f in res['added-facts']['facts']] + self.deck.addFactTags(fids, self.tagsToAdd) + self.total = len(res['added-facts']['facts']) + src.s.rollback() + self.deck.flushMod() + + def _clearDeleted(self, sum): + sum['delcards'] = [] + sum['delfacts'] = [] + sum['delmodels'] = [] diff --git a/anki/importing/csv.py b/anki/importing/csv.py new file mode 100644 index 000000000..827ca4faa --- /dev/null +++ b/anki/importing/csv.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Importing CSV/TSV files +======================== +""" +__docformat__ = 'restructuredtext' + +import codecs +from anki.importing import Importer, ForeignCard +from anki.lang import _ +from anki.errors import * + +class TextImporter(Importer): + + patterns = ("\t", ";") + + def __init__(self, *args): + Importer.__init__(self, *args) + self.lines = None + + def foreignCards(self): + self.parseTopLine() + # process all lines + log = [] + cards = [] + lineNum = 0 + ignored = 0 + for line in self.lines: + lineNum += 1 + if not line.strip(): + # ignore blank lines + continue + try: + fields = self.parseLine(line) + except ValueError: + log.append(_("Line %(line)d doesn't match pattern '%(pat)s'") + % { + 'line': lineNum, + 'pat': pattern, + }) + ignored += 1 + continue + if len(fields) != self.numFields: + log.append(_( + "Line %(line)d had %(num1)d fields," + " expected %(num2)d") % { + "line": lineNum, + "num1": len(fields), + "num2": self.numFields, + }) + ignored += 1 + continue + card = self.cardFromFields(fields) + cards.append(card) + self.log = log + self.ignored = ignored + return cards + + def parseTopLine(self): + "Parse the top line and determine the pattern and number of fields." + # load & look for the right pattern + self.cacheFile() + # look for the first non-blank line + l = None + for line in self.lines: + ret = line.strip() + if ret: + l = line + break + if not l: + raise ImportFormatError(type="emptyFile", + info=_("The file had no non-empty lines.")) + found = False + for p in self.patterns: + if p in l: + pattern = p + fields = l.split(p) + numFields = len(fields) + found = True + break + if not found: + fmtError = _( + "Couldn't find pattern. The file should be a series " + "of lines separated by tabs or semicolons.") + raise ImportFormatError(type="invalidPattern", + info=fmtError) + self.pattern = pattern + self.setNumFields(line) + + def cacheFile(self): + "Read file into self.lines if not already there." + if not self.lines: + self.lines = self.readFile() + + def readFile(self): + f = codecs.open(self.file, encoding="utf-8") + try: + data = f.readlines() + except UnicodeDecodeError, e: + raise ImportFormatError(type="encodingError", + info=_("The file was not in UTF8 format.")) + if not data: + return [] + if data[0].startswith(unicode(codecs.BOM_UTF8, "utf8")): + data[0] = data[0][1:] + # remove comment char + lines = [l for l in data if not l.lstrip().startswith("#")] + return lines + + def fields(self): + "Number of fields." + self.parseTopLine() + return self.numFields + + def setNumFields(self, line): + self.numFields = len(self.parseLine(line)) + + def parseLine(self, line): + fields = line.split(self.pattern) + fields = [f.strip() for f in fields] + return fields + + def cardFromFields(self, fields): + card = ForeignCard() + card.fields.extend(fields) + return card diff --git a/anki/importing/mnemosyne10.py b/anki/importing/mnemosyne10.py new file mode 100644 index 000000000..640f0f473 --- /dev/null +++ b/anki/importing/mnemosyne10.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Importing Mnemosyne 1.0 decks +============================== +""" +__docformat__ = 'restructuredtext' + +import sys, pickle, time, re +from anki.importing import Importer, ForeignCard +from anki.errors import * + +class Mnemosyne10Importer(Importer): + + multipleCardsAllowed = False + + def foreignCards(self): + # empty objects so we can load the native mnemosyne file + class MnemosyneModule(object): + class StartTime: + pass + class Category: + pass + class Item: + pass + for module in ('mnemosyne', + 'mnemosyne.core', + 'mnemosyne.core.mnemosyne_core'): + sys.modules[module] = MnemosyneModule() + try: + file = open(self.file, "rb") + except (IOError, OSError), e: + raise ImportFormatError(type="systemError", + info=str(e)) + header = file.readline().strip() + if (header != "--- Mnemosyne Data Base --- Format Version 1 ---" and + header != "--- Mnemosyne Data Base --- Format Version 2 ---"): + raise ImportFormatError(type="versionError", + info=header) + # read the structure in + try: + struct = pickle.load(file) + except (EOFError, KeyError): + raise ImportFormatError(type="invalidFile") + startTime = struct[0].time + daysPassed = (time.time() - startTime) / 86400.0 + # gather cards + cards = [] + for item in struct[2]: + card = ForeignCard() + card.fields.append(self.fudgeText(item.q)) + card.fields.append(self.fudgeText(item.a)) + # scheduling data + card.interval = item.next_rep - item.last_rep + secDelta = (item.next_rep - daysPassed) * 86400.0 + card.due = card.nextTime = time.time() + secDelta + # for some reason mnemosyne starts cards off on 1 instead of 0 + card.successive = max( + (item.acq_reps_since_lapse + item.ret_reps_since_lapse -1), 0) + card.yesCount = (item.acq_reps + item.ret_reps) - 1 + card.noCount = item.lapses + card.reps = card.yesCount + card.noCount + if item.cat.name != u"": + card.tags = item.cat.name + cards.append(card) + return cards + + def fields(self): + return 2 + + def fudgeText(self, text): + text = text.replace("\n", "
") + text = re.sub('', '[sound:\\1]', text) + return text diff --git a/anki/importing/wcu.py b/anki/importing/wcu.py new file mode 100644 index 000000000..16a8e2baa --- /dev/null +++ b/anki/importing/wcu.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Author Chris Aakre +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Importing WCU files +==================== +""" +__docformat__ = 'restructuredtext' + +import codecs +from anki.importing import Importer, ForeignCard +from anki.lang import _ +from anki.errors import * + +class WCUImporter(Importer): + def __init__(self, *args): + Importer.__init__(self, *args) + self.lines = None + self.numFields=int(2) + + def foreignCards(self): + from xml.dom import minidom, Node + cards = [] + f = None + try: + f = codecs.open(self.file, encoding="utf-8") + except: + raise ImportFormatError(type="encodingError", info=_("The file was not in UTF8 format.")) + f.close() + def wcuwalk(parent, cards, level=0): + for node in parent.childNodes: + if node.nodeType == Node.ELEMENT_NODE: + myCard=ForeignCard() + if node.attributes.has_key("QuestionPicture"): + question = [unicode('
'+node.attributes.get("Question").nodeValue)] + else: + question = [unicode(node.attributes.get("Question").nodeValue)] + if node.attributes.has_key("AnswerPicture"): + answer = [unicode('
'+node.attributes.get("Answer").nodeValue)] + else: + answer = [unicode(node.attributes.get("Answer").nodeValue)] + myCard.fields.extend(question) + myCard.fields.extend(answer) + cards.append(myCard) + wcuwalk(node, cards, level+1) + + def importwcu(file): + wcuwalk(minidom.parse(file).documentElement,cards) + importwcu(self.file) + return cards + + def fields(self): + return self.numFields + + def setNumFields(self): + self.numFields = int(2) diff --git a/anki/lang.py b/anki/lang.py new file mode 100644 index 000000000..ff24518d0 --- /dev/null +++ b/anki/lang.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Internationalisation +===================== +""" +__docformat__ = 'restructuredtext' + +import os, sys +import gettext +import threading + +threadLocal = threading.local() + +# global defaults +currentLang = None +currentTranslation = None + +def localTranslation(): + "Return the translation local to this thread, or the default." + if getattr(threadLocal, 'currentTranslation', None): + return threadLocal.currentTranslation + else: + return currentTranslation + +def _(str): + return localTranslation().ugettext(str) + +def ngettext(single, plural, n): + return localTranslation().ungettext(single, plural, n) + +def setLang(lang, local=True): + base = os.path.dirname(os.path.abspath(__file__)) + localeDir = os.path.join(base, "locale") + if not os.path.exists(localeDir): + localeDir = os.path.join( + os.path.dirname(sys.argv[0]), "locale") + trans = gettext.translation('libanki', localeDir, + languages=[lang], + fallback=True) + if local: + threadLocal.currentLang = lang + threadLocal.currentTranslation = trans + else: + global currentLang, currentTranslation + currentLang = lang + currentTranslation = trans + +def getLang(): + "Return the language local to this thread, or the default." + if getattr(threadLocal, 'currentLang', None): + return threadLocal.currentLang + else: + return currentLang + +if not currentTranslation: + setLang("en_US", local=False) diff --git a/anki/latex.py b/anki/latex.py new file mode 100644 index 000000000..039215025 --- /dev/null +++ b/anki/latex.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Latex support +============================== +""" +__docformat__ = 'restructuredtext' + +import re, tempfile, os, sys, subprocess +from htmlentitydefs import entitydefs +try: + import hashlib + md5 = hashlib.md5 +except ImportError: + import md5 + md5 = md5.new + +latexPreamble = ("\\documentclass[12pt]{article}\n" + "\\special{papersize=3in,5in}" + "\\usepackage[utf8]{inputenc}" + "\\pagestyle{empty}\n" + "\\begin{document}") +latexPostamble = "\\end{document}" +latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"] + +regexps = { + "standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE), + "expression": re.compile(r"\[\$\](.+?)\[/\$\]", re.DOTALL | re.IGNORECASE), + "math": re.compile(r"\[\$\$\](.+?)\[/\$\$\]", re.DOTALL | re.IGNORECASE), + } + +tmpdir = tempfile.mkdtemp(prefix="anki-latex") + +# add standard tex install location to osx +if sys.platform == "darwin": + os.environ['PATH'] += ":/usr/texbin" + +def renderLatex(deck, text): + "Convert TEXT with embedded latex tags to image links." + for match in regexps['standard'].finditer(text): + text = text.replace(match.group(), imgLink(deck, match.group(1))) + for match in regexps['expression'].finditer(text): + text = text.replace(match.group(), imgLink( + deck, "$" + match.group(1) + "$")) + for match in regexps['math'].finditer(text): + text = text.replace(match.group(), imgLink( + deck, + "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}")) + return text + +def stripLatex(text): + for match in regexps['standard'].finditer(text): + text = text.replace(match.group(), "") + for match in regexps['expression'].finditer(text): + text = text.replace(match.group(), "") + for match in regexps['math'].finditer(text): + text = text.replace(match.group(), "") + return text + +def call(*args, **kwargs): + try: + o = subprocess.Popen(*args, **kwargs) + except OSError: + # command not found + return -1 + while 1: + try: + ret = o.wait() + except OSError: + # interrupted system call + continue + break + return ret + +def imgLink(deck, latex): + "Parse LATEX and return a HTML image representing the output." + for match in re.compile("&([a-z]+);", re.IGNORECASE).finditer(latex): + if match.group(1) in entitydefs: + latex = latex.replace(match.group(), entitydefs[match.group(1)]) + latex = re.sub("", "\n", latex) + latex = latex.encode("utf-8") + imageFile = "latex-%s.png" % md5(latex).hexdigest() + imagePath = os.path.join(deck.mediaDir(create=True), imageFile) + imagePath = imagePath.encode(sys.getfilesystemencoding()) + if not os.path.exists(imagePath): + log = open(os.path.join(tmpdir, "latex_log.txt"), "w+") + texpath = os.path.join(tmpdir, "tmp.tex") + texfile = file(texpath, "w") + texfile.write(latexPreamble + "\n") + texfile.write(latex + "\n") + texfile.write(latexPostamble + "\n") + texfile.close() + texpath = texpath.encode(sys.getfilesystemencoding()) + oldcwd = os.getcwd() + if sys.platform == "win32": + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + si = None + try: + os.chdir(tmpdir) + errmsg = _("Error executing 'latex' or 'dvipng' - are they installed?") + if call(["latex", "-interaction=nonstopmode", + texpath], stdout=log, stderr=log, startupinfo=si): + return errmsg + if call(latexDviPngCmd + ["tmp.dvi", "-o", imagePath], + stdout=log, stderr=log, startupinfo=si): + return errmsg + finally: + os.chdir(oldcwd) + return '' % imageFile diff --git a/anki/locale/libanki_cs_CZ.po b/anki/locale/libanki_cs_CZ.po new file mode 100644 index 000000000..00f045e3e --- /dev/null +++ b/anki/locale/libanki_cs_CZ.po @@ -0,0 +1,744 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: 2008-01-01 19:00+0100\n" +"Last-Translator: Michal Čadil \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n>1;\n" + +#: deck.py:483 +#, python-format +msgid "" +"\n" +"

Congratulations!

You have finished the deck for now.

\n" +"%(next)s\n" +"

\n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
\n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" +"\n" +"Formát vhodný pro Heisigovo \"Remembering the Kanji\".\n" +"Jste testováni z převodu slova na kanji.\n" +"\n" +"Rozvržení testu je založeno na skvělé práci na\n" +"http://kanji.koohii.com/\n" +"\n" +"Odkaz v otázce bude zobrazovat příběhy zaslané\n" +"uživateli. Je požadováno přihlášení, které je zdarma." + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" +"\n" +"Vložte anglický výraz, který se chcete naučit do pole 'Výraz'.\n" +"Vložte popis v japonštině nebo angličtině do pole 'Význam'." + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" +"\n" +"Pole čtení je ve výchozím nastavení automaticky generováno a\n" +"ukazuje výslovnost výrazu. Pro slova, která jsou normálně psána\n" +"v hiragana nebo katakana a nevyžadují výslovnost, můžete vložit\n" +"slovo do pole významu a pole čtení nechat prázdné.\n" +"Výslovnost nebude automaticky generována pouze pro slova psaná\n" +"v hiragana nebo katakana.\n" +"\n" +"Automatické generování významu není bezchybné a před vložením\n" +"karty by měl být význam překontrolován." + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "%(count)s %(gradename)s kanji." + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "" + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "%0.1f sekund" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "%s dní" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "%s den" +msgstr[1] "%s dní" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "%s hodina" +msgstr[1] "%s hodin" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "%s minuta" +msgstr[1] "%s minut" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "%s měsíc" +msgstr[1] "%s měsíců" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "%s sekunda" +msgstr[1] "%s sekund" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "%s rok" +msgstr[1] "%s let" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, fuzzy, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "
Karty starší než týden: %(old)d (%(oldP)0.2f%%)
" + +#: stats.py:367 +#, fuzzy, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "Přidané karty: %(a)d denně, %(b)d měsíčně
" + +#: stats.py:356 +#, fuzzy, python-format +msgid "%0.0f days" +msgstr "Průměrný čas dalšího opakování: %0.0f dní
" + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, fuzzy, python-format +msgid "%0.1f cards/day" +msgstr "Průměrná zátěž: %0.1f karet denně
" + +#: stats.py:354 +msgid "Averages
" +msgstr "" + +#: stats.py:333 +msgid "Card counts
" +msgstr "" + +#: stats.py:341 +msgid "Correct answers
" +msgstr "" + +#: stats.py:522 +#, fuzzy, python-format +msgid "

Kanji statistics

The %d seen cards in this deck contain:" +msgstr "

Statistika kanji

%d karet v tomto balíku obsahuje:" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "
  • %d celkem unikátních kanji.<" + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" +"Běžná kartička s přední a zadní stranou.\n" +"Ve výchozím nastavení se ukazuje zadní strana\n" +"a hádá se přední.\n" +"Prosím zvaže úpravu tohoto modelu spíše než jeho použití\n" +"tak jak je nadefinován: názvy polí jako \"výraz\" jsou\n" +"jasnější než \"přední a zadní strana\" a zajistí, že Vaše\n" +"kartičky budou konzistentní." + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "Popis ve Vašem jazyce nebo v kantonštině" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "Popis ve Vašem jazyce nebo v japonštině" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "Popis ve Vašem jazyce nebo v mandarínštině" + +#: stdmodels.py:38 +msgid "A question." +msgstr "Otázka." + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "Slovo nebo výraz psaný v hanzi." + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "Slovo nebo výraz psaný v kanji." + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "Testujte aktivně Vaše znalosti vytvořením výrazu" + +#: stats.py:256 +msgid "Added" +msgstr "Přidáno" + +#: stats.py:373 +msgid "Added last month" +msgstr "" + +#: stats.py:370 +msgid "Added last week" +msgstr "" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "Balík anki" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "Balíky anki" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "Ve výchozím nastavení automaticky generováno." + +#: stats.py:357 +#, fuzzy +msgid "Average reps" +msgstr "Průměrný čas" + +#: stats.py:277 +msgid "Average time" +msgstr "Průměrný čas" + +#: stats.py:367 +#, fuzzy +msgid "Avg. added" +msgstr "Přidáno" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "Ze zadní strany na přední" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "kantonština" + +#: stats.py:290 +msgid "Card model tags" +msgstr "Štítky modelů kartiček" + +#: stats.py:289 +msgid "Card tags" +msgstr "Štítky kartiček" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +msgid "Cards" +msgstr "Kartičky" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "Kartičky v textovém souboru odděleném tabulátorem (*.txt)" + +#: models.py:25 +msgid "Center" +msgstr "Střed" + +#: stats.py:259 +msgid "Changed" +msgstr "Změněno" + +#: stats.py:274 +msgid "Correct count" +msgstr "Známé kartičky" + +#: stats.py:270 +#, fuzzy +msgid "Current factor" +msgstr "Současný interval" + +#: stats.py:266 +msgid "Current interval" +msgstr "Současný interval" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "" + +#: graphs.py:136 +msgid "Day" +msgstr "" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "Statistika balíku" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "Balík vytvořen před: %s
    " + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "" + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "" + +#: stdmodels.py:100 +msgid "English" +msgstr "angličtina" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "" + +#: stats.py:294 +msgid "Fact tags" +msgstr "Štítky výrazů" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "výrazy oddělené tabulátory (*.txt)" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "" + +#: stats.py:379 +#, fuzzy +msgid "First last month" +msgstr "Poprvé" + +#: stats.py:376 +#, fuzzy +msgid "First last week" +msgstr "Poprvé" + +#: stats.py:258 +#, fuzzy +msgid "First review" +msgstr "Poprvé" + +#: stats.py:282 +msgid "First time" +msgstr "Poprvé" + +#: stats.py:346 +#, fuzzy, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "Kartičky známé na první pokus: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "Z anglického výrazu do významu" + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "Z klíčového slova do kanji" + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "Z významu do anglického výrazu" + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "Ze zadní strany na přední" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "Heisig" + +#: stats.py:356 +#, fuzzy +msgid "Interval" +msgstr "Poslední interval" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "japonsky" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "úrovně Jouyou" + +#: stats.py:271 +msgid "Last factor" +msgstr "" + +#: stats.py:269 +msgid "Last interval" +msgstr "Poslední interval" + +#: models.py:26 +msgid "Left" +msgstr "Zbývá" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "mandarínština" + +#: stats.py:286 +msgid "Mature" +msgstr "Starší karty" + +#: graphs.py:210 +#, fuzzy +msgid "Mature cards" +msgstr "Starší karty" + +#: stats.py:342 +#, fuzzy, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "Známé starší kartičky: %(gOldYes%)0.1f%% (%(gOldYes)d of %(gOldTotal)d)
    " + +#: stats.py:334 +#, fuzzy, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "
    Karty starší než týden: %(old)d (%(oldP)0.2f%%)
    " + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "" + +#: stats.py:292 +msgid "Model tags" +msgstr "Štítky modelu" + +#: graphs.py:208 +msgid "New cards" +msgstr "Nové kartičky" + +#: stats.py:265 +msgid "Next due" +msgstr "Příští opakování" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "Nejprve vložte nějaké kartičky" + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "Opakovaně správně" + +#: stats.py:365 +#, fuzzy +msgid "Reps last month" +msgstr "%s měsíc" + +#: stats.py:363 +msgid "Reps last week" +msgstr "" + +#: stats.py:361 +#, fuzzy +msgid "Reps next month" +msgstr "%s měsíc" + +#: stats.py:359 +msgid "Reps next week" +msgstr "" + +#: stats.py:272 +msgid "Review count" +msgstr "Počet opakování" + +#: models.py:27 +msgid "Right" +msgstr "Správně" + +#: deck.py:1824 +msgid "Show new cards after all other cards" +msgstr "" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "" + +#: stats.py:287 +msgid "State" +msgstr "Stav" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "Otestujte Vaši schopnost uhodnout výraz" + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "Textové soubory (*.txt)" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "Odpověď" + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "" + +#: deck.py:431 +#, fuzzy +msgid "The deck is empty. Please add some cards." +msgstr "Nejprve vložte nějaké kartičky" + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "" + +#: stats.py:331 +#, fuzzy, python-format +msgid "Total number of cards: %d

    " +msgstr "Celkový pošet kartiček: %d
    " + +#: stats.py:279 +msgid "Total time" +msgstr "Celkový čas" + +#: deck.py:702 +msgid "Unknown" +msgstr "Neznámý" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "Zatím nezobrazené kartičky: %(new)d (%(newP)0.2f%%)

    " + +#: stats.py:284 +msgid "Young" +msgstr "Nové" + +#: graphs.py:209 +msgid "Young cards" +msgstr "Nové kartičky" + +#: stats.py:344 +#, fuzzy, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "Známé nové kartičky: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "Nové kartičky: %(young)d (%(youngP)0.2f%%)
    " + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "[prázdné]" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "[neplatný formát; viz vlastnosti modelu]" + +#: deck.py:463 +msgid "a short time" +msgstr "krátký čas" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "za %s" + +#: deck.py:448 +msgid "unknown" +msgstr "neznámý" + +#~ msgid "%x at %H:%M" +#~ msgstr "%x v %H:%M" + +#~ msgid "Colon/semicolon/tab-separated text file (*.*)" +#~ msgstr "Textový soubor oddělený čárkou/středníkem (*.*)" + +#~ msgid "Couldn't find pattern. The file should be a series of lines separated by colons, semicolons or tabs." +#~ msgstr "Vzor nenalezen. Soubor musí být řádky s výrazy oddělenými čárkami, středníky nebo tabulátory." + +#~ msgid "Deck%" +#~ msgstr "Balík%" + +#~ msgid "Fc .pending file (*.pending *.imported)" +#~ msgstr "Soubor .pendig (*.pending *.imported)" + +#~ msgid "Import: duplicate" +#~ msgstr "Import: duplikát" + +#~ msgid "Import: missing field" +#~ msgstr "Import: chybějící pole" + +#~ msgid "Import: same %s" +#~ msgstr "Import: stejné %s" + +#~ msgid "Line %(line)d doesn't match pattern '%(pat)s'" +#~ msgstr "Řádek %(line)d neodpovídá vzoru '%(pat)s" + +#~ msgid "Line %(line)d had %(num1)d fields, expected %(num2)d" +#~ msgstr "Řádek %(line)d měl %(num1)d polí, očekáváno %(num2)d" + +#~ msgid "Mnemosyne file (*.mem)" +#~ msgstr "Soubor Mnemosyne (*.mem)" + +#~ msgid "Model" +#~ msgstr "Model" + +#~ msgid "Old Anki 0.2.x file (*.fc)" +#~ msgstr "Soubor Anki verye 0.2.x (*.fc)" + +#~ msgid "Old cards" +#~ msgstr "Staré kartičky" + +#~ msgid "Order by input date" +#~ msgstr "Seřadit podle data vložení" + +#~ msgid "Other" +#~ msgstr "Jiné" + +#~ msgid "Period" +#~ msgstr "Období" + +#~ msgid "Random" +#~ msgstr "Náhodně" + +#~ msgid "The file had no non-empty lines." +#~ msgstr "Pole nemá žádné neprázdné řádky" + +#~ msgid "The file was not in UTF8 format." +#~ msgstr "Soubor nebyl ve formátu UTF8" + +#~ msgid "Untitled" +#~ msgstr "Bez názvu" + +#~ msgid "in %(a)s, on %(b)s" +#~ msgstr "za %(a), %(b)" diff --git a/anki/locale/libanki_de_DE.po b/anki/locale/libanki_de_DE.po new file mode 100644 index 000000000..e5bb38846 --- /dev/null +++ b/anki/locale/libanki_de_DE.po @@ -0,0 +1,678 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: deck.py:483 +#, python-format +msgid "" +"\n" +"

    Congratulations!

    You have finished the deck for now.

    \n" +"%(next)s\n" +"

    \n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
    \n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" +"\n" +"

    Glückwunsch!

    Du hast momentan alle Karten bearbeitet.

    \n" +"%(next)s\n" +"

    \n" +"Es gibt %(waiting)d\n" +"\n" +"wartende Karten.
    \n" +"Es gibt %(suspended)d\n" +"\n" +"ausgesetzte Karten." + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" +"\n" +"Ein Format welches sich zum Lernen von Heisig's \"Remembering the Kanji\" eignet.\n" +"Du musst das Kanji zum Schlüsselwort antworten.\n" +"\n" +"Das Format des Tests basiert auf\n" +"http://kanji.koohii.com/\n" +"\n" +"Der Hyperlink in den Fragen listet von Benutzern hinzugefügte Beispiele auf.\n" +"Ein kostenloser Login ist dafür notwendig." + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" +"\n" +"Gib den englischen Ausdruck, den Du lernen möchtest, im Feld 'Ausdruck' ein.\n" +"Gib im Feld 'Bedeutung' eine Beschreibung in Japanisch oder Englisch ein." + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "%(count)s %(gradename)s Kanji." + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "%(gradename)s: %(count)s von %(total)s (%(percent)0.1f%%)." + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "%0.1f Sekunden" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "vor %s" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "%s Tag" +msgstr[1] "%s Tage" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "%s Stunde" +msgstr[1] "%s Stunden" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "%s Minute" +msgstr[1] "%s Minuten" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "%s Monat" +msgstr[1] "%s Monate" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "%s Sekunde" +msgstr[1] "%s Sekunden" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "%s Jahr" +msgstr[1] "%s Jahre" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "%(a)d (%(b)0.1f/Tag)" + +#: stats.py:367 +#, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "%(a)d/Tag, %(b)d/Monat" + +#: stats.py:356 +#, python-format +msgid "%0.0f days" +msgstr "%0.0f Tage" + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, python-format +msgid "%0.1f cards/day" +msgstr "%0.1f Karten/Tag" + +#: stats.py:354 +msgid "Averages
    " +msgstr "Durchschnittswerte
    " + +#: stats.py:333 +msgid "Card counts
    " +msgstr "Anzahl Karten
    " + +#: stats.py:341 +msgid "Correct answers
    " +msgstr "Richtige Antworten
    " + +#: stats.py:522 +#, python-format +msgid "

    Kanji statistics

    The %d seen cards in this deck contain:" +msgstr "

    Kanji-Statistik

    Die %d gesehenen Karten in diesem Stapel enthalten:" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "
  • insgesamt %d verschiedene Kanji.
  • " + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Hinzugefügt/GeändertLokalAuf Server
    Karten%(lc)d%(rc)d
    Fakten%(lf)d%(rf)d
    Modelle%(lm)d%(rm)d
    " + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" +"Eine normale Karteikarte mit Vorderseite und Rückseite.\n" +"Fragen werden von der Vorderseite zur Rückseite gestellt.\n" +"\n" +"Passe dieses Modell nach Möglichkeit für Deine Zwecke an.\n" +"Feldnamen wie \"Aussprache\", \"Deutsch\" etc. sind\n" +"verständlicher als \"Vorderseite\" und \"Rückseite\". Das gewährleistet außerdem,\n" +"dass Deine neu erstellten Karten konsistent sind." + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "Eine Beschreibung in Deiner eigenen Sprache oder auf Kantonesisch" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "Eine Beschreibung in Deiner eigenen Sprache oder auf Japanisch" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "Eine Beschreibung in Deiner eigenen Sprache oder auf Mandarin" + +#: stdmodels.py:38 +msgid "A question." +msgstr "Eine Frage." + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "Ein Wort oder Ausdruck in Hanzi." + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "Ein Wort oder Ausdruck in Kanji." + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "Teste Deine Erinnerung, indem Du den Ausdruck hervorbringst" + +#: stats.py:256 +msgid "Added" +msgstr "Hinzugefügt" + +#: stats.py:373 +msgid "Added last month" +msgstr "Letzten Monat hinzugefügt" + +#: stats.py:370 +msgid "Added last week" +msgstr "Letzte Woche hinzugefügt" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "Anki Stapeldatei (*.anki)" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "Anki Stapeldateien (*.anki)" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" +"Morgen werden zur gleichen Zeit

    \n" +"- %(wait)d Karten zur Wiederholung bereit sein
    \n" +"- %(new)d\n" +"\n" +"neue Karten verfügbar sein" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "Standardmäßig automatisch generiert." + +#: stats.py:357 +msgid "Average reps" +msgstr "Durchschnittliche Wiederholungen" + +#: stats.py:277 +msgid "Average time" +msgstr "Durchschnittliche Zeit" + +#: stats.py:367 +msgid "Avg. added" +msgstr "Durchschnittlich hinzugefügt" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "Rückseite zu Vorderseite" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "Einfach" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "Kann den Stapel nicht lesen/schreiben" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "Kantonesisch" + +#: stats.py:290 +msgid "Card model tags" +msgstr "Kartenmodell-Tags" + +#: stats.py:289 +msgid "Card tags" +msgstr "Karten-Tags" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +msgid "Cards" +msgstr "Karten" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "Karten in Tabulator-getrennter Textdatei (*.txt)" + +#: models.py:25 +msgid "Center" +msgstr "Zentriert" + +#: stats.py:259 +msgid "Changed" +msgstr "Geändert" + +#: stats.py:274 +msgid "Correct count" +msgstr "Anzahl richtiger Antworten" + +#: stats.py:270 +msgid "Current factor" +msgstr "Aktueller Faktor" + +#: stats.py:266 +msgid "Current interval" +msgstr "Aktuelles Intervall" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "Datenbank-Datei defekt. Bitte von einem Backup wiederherstellen." + +#: graphs.py:136 +msgid "Day" +msgstr "Tag" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "Tage" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "Stapelstatistik" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "Alter des Stapels: %s
    " + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "Im Stapel fehlte ein Modell" + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "%d Karten mit fehlendem Faktum gelöscht" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "%d Karten ohne Modell gelöscht" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "%d ungenutzte Felder gelöscht" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "%d Fakten mit fehlenden Feldern gelöscht" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "%d Fakten ohne Karten gelöscht" + +#: stdmodels.py:100 +msgid "English" +msgstr "Englisch" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "Fehler beim Ausführen von 'latex' oder 'dvipng' - sind sie installiert?" + +#: stats.py:294 +msgid "Fact tags" +msgstr "Fakten-Tags" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "Fakten in Tabulator-getrennter Textdatei (*.txt)" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "Die Datei wird von einem anderen Prozess benutzt" + +#: stats.py:379 +msgid "First last month" +msgstr "Erstmalig letzten Monat" + +#: stats.py:376 +msgid "First last week" +msgstr "Erstmalig letzte Woche" + +#: stats.py:258 +msgid "First review" +msgstr "Erstmalig wiederholt" + +#: stats.py:282 +msgid "First time" +msgstr "Erstmalig" + +#: stats.py:346 +#, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "Erstmalig gesehene Karten: %(gNewYes%)0.1f%% (%(gNewYes)d von %(gNewTotal)d)

    " + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "Vom englischen Ausdruck zur Bedeutung." + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "Vom Schlüsselwort zum Kanji." + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "Von der Bedeutung zum englischen Ausdruck." + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "Vorderseite zu Rückseite" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "Heisig" + +#: stats.py:356 +msgid "Interval" +msgstr "Intervall" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "Japanisch" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "Jouyou-Level:" + +#: stats.py:271 +msgid "Last factor" +msgstr "Vorheriger Faktor" + +#: stats.py:269 +msgid "Last interval" +msgstr "Vorheriges Intervall" + +#: models.py:26 +msgid "Left" +msgstr "Links" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "Mandarin" + +#: stats.py:286 +msgid "Mature" +msgstr "Alt" + +#: graphs.py:210 +msgid "Mature cards" +msgstr "Alte Karten" + +#: stats.py:342 +#, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "Alte Karten: %(gMatureYes%)0.1f%% (%(gMatureYes)d von %(gMatureTotal)d)
    " + +#: stats.py:334 +#, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "Alte Karten: %(old)d (%(oldP)0.2f%%)
    " + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "Fehlende Mediendateien" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "Modell '%s' hat eine falsche Anzahl Karten" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "Modell '%s' hat eine falsche Anzahl Felder" + +#: stats.py:292 +msgid "Model tags" +msgstr "Modell-Tags" + +#: graphs.py:208 +msgid "New cards" +msgstr "Neue Karten" + +#: stats.py:265 +msgid "Next due" +msgstr "Wieder fällig" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "Bitte füge zuerst ein paar Karten hinzu.

    " + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "Wiederholt richtig" + +#: stats.py:365 +msgid "Reps last month" +msgstr "Wiederholungen letzten Monat" + +#: stats.py:363 +msgid "Reps last week" +msgstr "Wiederholungen letzte Woche" + +#: stats.py:361 +msgid "Reps next month" +msgstr "Wiederholungen nächsten Monat" + +#: stats.py:359 +msgid "Reps next week" +msgstr "Wiederholungen nächste Woche" + +#: stats.py:272 +msgid "Review count" +msgstr "Anzahl Wiederholungen" + +#: models.py:27 +msgid "Right" +msgstr "Rechts" + +#: deck.py:1824 +msgid "Show new cards after all other cards" +msgstr "Zeige neue Karten nach allen anderen Karten" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "Zeige neue Karten in der Reihenfolge des Hinzufügens" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "Zeige neue Karten in zufälliger Reihenfolge" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "Neue Karten über die Wiederholungen verteilen" + +#: stats.py:287 +msgid "State" +msgstr "Zustand" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "Teste Deine Fähigkeit, den Ausdruck zu erkennen" + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "Textdateien (*.txt)" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "Die Antwort." + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "Das aktuelle Modell war nicht vorhanden" + +#: deck.py:431 +msgid "The deck is empty. Please add some cards." +msgstr "Der Stapel ist leer. Bitte füge ein paar Karten hinzu." + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "Die nächste Karte wird in %s gezeigt" + +#: stats.py:331 +#, python-format +msgid "Total number of cards: %d

    " +msgstr "Gesamtanzahl Karten: %d

    " + +#: stats.py:279 +msgid "Total time" +msgstr "Gesamtzeit" + +#: deck.py:702 +msgid "Unknown" +msgstr "Unbekannt" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "Nicht gesehene Karten: %(new)d (%(newP)0.2f%%)

    " + +#: stats.py:284 +msgid "Young" +msgstr "Jung" + +#: graphs.py:209 +msgid "Young cards" +msgstr "Junge Karten" + +#: stats.py:344 +#, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "Junge Karten: %(gYoungYes%)0.1f%% (%(gYoungYes)d von %(gYoungTotal)d)
    " + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "Junge Karten: %(young)d (%(youngP)0.2f%%)
    " + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "[leer]" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "[ungültiges Format; siehe Modelleigenschaften]" + +#: deck.py:463 +msgid "a short time" +msgstr "eine kurze Zeit" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "in %s" + +#: deck.py:448 +msgid "unknown" +msgstr "unbekannt" diff --git a/anki/locale/libanki_es_ES.po b/anki/locale/libanki_es_ES.po new file mode 100644 index 000000000..23a05c6f4 --- /dev/null +++ b/anki/locale/libanki_es_ES.po @@ -0,0 +1,815 @@ +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Anki 0.9.7.7\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: 2008-09-03 02:48+0100\n" +"Last-Translator: Pcsl \n" +"Language-Team: Spanish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: Spanish\n" +"X-Poedit-Country: SPAIN\n" + +#: deck.py:483 +#, fuzzy, python-format +msgid "" +"\n" +"

    Congratulations!

    You have finished the deck for now.

    \n" +"%(next)s\n" +"

    \n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
    \n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" +"\n" +"

    ¡Felicidades!

    Ha finalizado el mazo por ahora.

    \n" +"%(next)s\n" +"

    \n" +"Hay %(waiting)d\n" +"\n" +"tarjetas espaciadas.
    \n" +"Hay %(suspended)d\n" +"\n" +"tarjetas suspendidas." + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" +"\n" +"Un formato idóneo para el libro \"Kanji para Recordar\" de Heisig.\n" +"Será examinado desde la palabra clave a los kanji.\n" +"\n" +"La plantilla está basada en el gran trabajo en\n" +"http://kanji.koohii.com/\n" +"\n" +"El enlace de la pregunta le mostrará historias hechas\n" +"por los usuarios. Se requiere una identificación gratuita." + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" +"\n" +"Introduzca la expresión en inglés que desea aprender en el campo 'Expresión.\n" +"Introduzca una descripción en japonés o inglés en el campo de 'Significado'." + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" +"\n" +"El campo de lectura se genera automáticamente por defecto,\n" +"y muestra la lectura de la expresión. Para las palabras que\n" +"se escriben normalmente en hiragana y katakana y no necesitan\n" +"una lectura, puede poner la palabra en el campo de expresión,\n" +"y dejar el campo de lectura en blanco. La lectura no\n" +"será creada automáticamente para palabras escritas\n" +"solamente en hiragana o katakana.\n" +"\n" +"Dése cuenta de que la creación automática de significados no es\n" +"perfecta, y debe ser comprobado antes de añadir significados." + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "%(count)s %(gradename)s kanji.s" + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "%(gradename)s: %(count)s de %(total)s (%(percent)0.1f%%)." + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "%0.1f segundos" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "hace %s" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "%s día" +msgstr[1] "%s días" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "%s hora" +msgstr[1] "%s horas" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "%s minuto" +msgstr[1] "%s minutos" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "%s mes" +msgstr[1] "%s meses" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "%s segundo" +msgstr[1] "%s segundos" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "%s año" +msgstr[1] "%s años" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, fuzzy, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "Tarjetas maduras: %(old)d (%(oldP)0.2f%%)
    " + +#: stats.py:367 +#, fuzzy, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "Añadidas: %(a)d por día, %(b)d por mes
    " + +#: stats.py:356 +#, fuzzy, python-format +msgid "%0.0f days" +msgstr "Intervalo: %0.0f días
    " + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, fuzzy, python-format +msgid "%0.1f cards/day" +msgstr "Volumen de trabajo: %0.1f tarjetas/día
    " + +#: stats.py:354 +msgid "Averages
    " +msgstr "Medias
    " + +#: stats.py:333 +msgid "Card counts
    " +msgstr "Número de tarjetas
    " + +#: stats.py:341 +msgid "Correct answers
    " +msgstr "Respuestas correctas
    " + +#: stats.py:522 +#, python-format +msgid "

    Kanji statistics

    The %d seen cards in this deck contain:" +msgstr "

    Estadísticas de kanjis

    Las %d tarjetas vistas en este mazo contienen:" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "
  • %d kanjis únicos totales.
  • " + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" +"Una tarjeta flash básica con la cara y el reverso.\n" +"Las preguntas serán examinadas por defecto, desde la cara al reverso .\n" +"\n" +"Tenga en cuenta que puede personalizar el modelo, en lugar de\n" +"dejarlo tal cual: campos como \"expresión\" son\n" +"más clarificadores que \"cara\" y \"reverso\", y se asegurará de\n" +"que sus entradas sean más consecuentes." + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "Una descripición en su lengua materna, o cantonés" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "Una descripición en su lengua materna, o japonés" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "Una descripición en su lengua materna, o mMandarín" + +#: stdmodels.py:38 +msgid "A question." +msgstr "Una pregunta." + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "Una palabra o expresión escrita en Hanzi." + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "Una palabra o expresión escrita en Kanji." + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "Comprueba su memoria activamente escribiendo la expresión de destino" + +#: stats.py:256 +msgid "Added" +msgstr "Añadido" + +#: stats.py:373 +msgid "Added last month" +msgstr "" + +#: stats.py:370 +msgid "Added last week" +msgstr "" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "Mazo Anki (*.anki)" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "Mazos Anki (*.anki)" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "Creado automáticamente por defecto." + +#: stats.py:357 +#, fuzzy +msgid "Average reps" +msgstr "Tiempo medio" + +#: stats.py:277 +msgid "Average time" +msgstr "Tiempo medio" + +#: stats.py:367 +#, fuzzy +msgid "Avg. added" +msgstr "Añadido" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "Desde el reverso a la cara" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "Básico" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "No se puede leer/escribir el mazo" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "Cantonés" + +#: stats.py:290 +msgid "Card model tags" +msgstr "Etiquetas del modelo de tarjetas" + +#: stats.py:289 +msgid "Card tags" +msgstr "Etiquetas de la tarjeta" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +msgid "Cards" +msgstr "Tarjetas" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "Tarjetas en un archivo de texto separado por tabulador (*.txt)" + +#: models.py:25 +msgid "Center" +msgstr "Centrar" + +#: stats.py:259 +msgid "Changed" +msgstr "Cambiado" + +#: stats.py:274 +msgid "Correct count" +msgstr "Veces correcto" + +#: stats.py:270 +msgid "Current factor" +msgstr "Factor actual" + +#: stats.py:266 +msgid "Current interval" +msgstr "Intervalo actual" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "Archivo de Base de Datos dañado. Restaurela de la copia de seguridad." + +#: graphs.py:136 +msgid "Day" +msgstr "" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "Estadísticas del Mazo" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "Mazo creado: Hace %s
    " + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "El mazo carecía de un modelo." + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "Borradas %d tarjetas que carecían de hecho" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "Borradas %d tarjetas que carecían de modelo de tarjeta" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "Borrados %d campos pendientes" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "Borrados %d hechos con campos faltantes" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "Borrados %d hechos sin tarjetas" + +#: stdmodels.py:100 +msgid "English" +msgstr "Inglés" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "Error ejecutando 'latex' o 'dvipng' - ¿Están instalados?" + +#: stats.py:294 +msgid "Fact tags" +msgstr "Etiquetas de hechos" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "Hechos en un archivo de texto separado por tabulador (*.txt)" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "El archivo está en uso por otro proceso" + +#: stats.py:379 +#, fuzzy +msgid "First last month" +msgstr "Primera vez" + +#: stats.py:376 +#, fuzzy +msgid "First last week" +msgstr "Primera vez" + +#: stats.py:258 +msgid "First review" +msgstr "Primer repaso" + +#: stats.py:282 +msgid "First time" +msgstr "Primera vez" + +#: stats.py:346 +#, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "Vistas por primera vez: %(gNewYes%)0.1f%% (%(gNewYes)d de %(gNewTotal)d)

    " + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "Desde la expresión en inglés al significado." + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "Desde la palabra clave a el Kanji." + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "Desde el significado a la expresión en inglés." + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "Desde la cara al reverso" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "Heisig" + +#: stats.py:356 +#, fuzzy +msgid "Interval" +msgstr "Último intervalo" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "Japonés" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "Niveles Jouyou:" + +#: stats.py:271 +msgid "Last factor" +msgstr "Último factor" + +#: stats.py:269 +msgid "Last interval" +msgstr "Último intervalo" + +#: models.py:26 +msgid "Left" +msgstr "Izquierda" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "Mandarín" + +#: stats.py:286 +msgid "Mature" +msgstr "Madura" + +#: graphs.py:210 +msgid "Mature cards" +msgstr "Tarjetas maduras" + +#: stats.py:342 +#, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "Tarjetas maduras: %(gMatureYes%)0.1f%% (%(gMatureYes)d de %(gMatureTotal)d)
    " + +#: stats.py:334 +#, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "Tarjetas maduras: %(old)d (%(oldP)0.2f%%)
    " + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "El modelo '%s' tenía un número incorrecto de modelos de tarjeta" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "El modelo '%s' tenía un número incorrecto de modelos de campo" + +#: stats.py:292 +msgid "Model tags" +msgstr "Etiquetas del modelo" + +#: graphs.py:208 +msgid "New cards" +msgstr "Nueva tarjeta" + +#: stats.py:265 +msgid "Next due" +msgstr "Siguiente repaso dentro de" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "Por favor, añada algunas tarjetas antes.

    " + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "Repetido correctamente" + +#: stats.py:365 +#, fuzzy +msgid "Reps last month" +msgstr "%s mes" + +#: stats.py:363 +msgid "Reps last week" +msgstr "" + +#: stats.py:361 +#, fuzzy +msgid "Reps next month" +msgstr "%s mes" + +#: stats.py:359 +msgid "Reps next week" +msgstr "" + +#: stats.py:272 +msgid "Review count" +msgstr "Veces repasado" + +#: models.py:27 +msgid "Right" +msgstr "Derecha" + +#: deck.py:1824 +#, fuzzy +msgid "Show new cards after all other cards" +msgstr "Mostrar nuevas tarjetas en el orden en el que fueron añadidas" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "Mostrar nuevas tarjetas en el orden en el que fueron añadidas" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "Mostrar nuevas tarjetas de forma aleatoria" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "" + +#: stats.py:287 +msgid "State" +msgstr "Estado" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "Compruebe su habilidad para reconocer la expresión de destino" + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "Archivos de texto (*.txt)" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "La respuesta." + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "El modelo actual no existía" + +#: deck.py:431 +msgid "The deck is empty. Please add some cards." +msgstr "El mazo está vacío. Por favor, añada algunas tarjetas." + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "La siguiente tarjeta se mostrará en %s" + +#: stats.py:331 +#, python-format +msgid "Total number of cards: %d

    " +msgstr "Número total de tarjetas: %d

    " + +#: stats.py:279 +msgid "Total time" +msgstr "Tiempo total" + +#: deck.py:702 +msgid "Unknown" +msgstr "Desconocido" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "Tarjetas no mostradas: %(new)d (%(newP)0.2f%%)

    " + +#: stats.py:284 +msgid "Young" +msgstr "Joven" + +#: graphs.py:209 +msgid "Young cards" +msgstr "Tarjetas jóvenes" + +#: stats.py:344 +#, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "Tarjetas jóvenes: %(gYoungYes%)0.1f%% (%(gYoungYes)d de %(gYoungTotal)d)
    " + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "Tarjetas jóvenes: %(young)d (%(youngP)0.2f%%)
    " + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "[vacío]" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "[formato no válido; vea las propiedades del modelo]" + +#: deck.py:463 +msgid "a short time" +msgstr "en poco tiempo" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "en %s" + +#: deck.py:448 +msgid "unknown" +msgstr "desconocido" + +#~ msgid "" +#~ "\n" +#~ "Two cards: one tests meaning -> imperfective + perfective,\n" +#~ "and one tests imperfective -> perfective." +#~ msgstr "" +#~ "\n" +#~ "Dos tarjetas: una pregunta el significado -> imperfectivo + perfectivo,\n" +#~ "y la otra pregunta imperfectivo -> perfectivo." + +#~ msgid "" +#~ "\n" +#~ "Two cards: one tests production, one recognition." +#~ msgstr "" +#~ "\n" +#~ "Dos tarjetas: una le examina activamente, la otra le examina en el reconocimiento." + +#~ msgid "%(a)d.%(b)df day" +#~ msgstr "%(a)d.%(b)df días" + +#~ msgid "%(a)d.%(b)df hour" +#~ msgstr "%(a)d.%(b)df horas" + +#~ msgid "%(a)d.%(b)df minute" +#~ msgstr "%(a)d.%(b)df minutos" + +#~ msgid "%(a)d.%(b)df second" +#~ msgstr "%(a)d.%(b)df segundos" + +#~ msgid "%(a)d.%(b)df year" +#~ msgstr "%(a)d.%(b)df años" + +#~ msgid "%s days" +#~ msgstr "%s días" + +#~ msgid "%x at %H:%M" +#~ msgstr "%x a las %H:%M" + +#~ msgid "A random position in the deck" +#~ msgstr "Posición aleatoria en el mazo" + +#~ msgid "At the end of the deck" +#~ msgstr "Al final del mazo" + +#~ msgid "At the same time tomorrow, there will be %d cards waiting" +#~ msgstr "A la misma hora mañana, habrá %d tarjetas que responder" + +#~ msgid "At the start of the deck" +#~ msgstr "Al principio del mazo" + +#~ msgid "Can't read/write directory" +#~ msgstr "No se puede leer/escribir el directorio" + +#~ msgid "Colon/semicolon/tab-separated text file (*.*)" +#~ msgstr "Archivo de texto separado por Coma/punto y coma/tabulador (*.*)" + +#~ msgid "Couldn't find pattern. The file should be a series of lines separated by colons, semicolons or tabs." +#~ msgstr "No se ajusta a la plantilla. El archivo debe estar compuesto por una serie de líneas separadas por punto, coma y punto o tabulador." + +#~ msgid "Deck%" +#~ msgstr "Mazo%" + +#~ msgid "Error executing 'latex' - is it installed?" +#~ msgstr "Error ejecutando 'latex' -¿Está instalado?" + +#~ msgid "Fc .pending file (*.pending *.imported)" +#~ msgstr "Archivo Fc .pendiente (*.pending *.imported)" + +#~ msgid "From imperfective to perfective" +#~ msgstr "De Imperfectivo a perfectivo" + +#~ msgid "From the Russian expression to the meaning." +#~ msgstr "Desde la expresión en ruso al significado." + +#~ msgid "From the meaning to an imperfective+perfective pair." +#~ msgstr "Desde el significado a el par imperfectivo+perfectivo." + +#~ msgid "From the meaning to the Russian expression." +#~ msgstr "Desde el signficado a la expresión en ruso." + +#~ msgid "Import: duplicate" +#~ msgstr "Importar: duplicado" + +#~ msgid "Import: missing field" +#~ msgstr "Importar: campo no presente" + +#~ msgid "Import: same %s" +#~ msgstr "Importar: mismo %s" + +#~ msgid "Line %(line)d doesn't match pattern '%(pat)s'" +#~ msgstr "La línea %(line)d no se ajusta a la plantilla '%(pat)s'" + +#~ msgid "Line %(line)d had %(num1)d fields, expected %(num2)d" +#~ msgstr "La línea %(line)d tiene %(num1)d campos, se esperaban %(num2)d" + +#~ msgid "Mnemosyne file (*.mem)" +#~ msgstr "Archivo Mnemosyne (*.mem)" + +#~ msgid "Model" +#~ msgstr "Modelo" + +#~ msgid "Old Anki 0.2.x file (*.fc)" +#~ msgstr "Antiguo archivo Anki 0.2.x (*.fc)" + +#~ msgid "Old cards" +#~ msgstr "Tarjetas viejas" + +#~ msgid "Other" +#~ msgstr "Otro" + +#~ msgid "Period" +#~ msgstr "Periodo" + +#~ msgid "Russian" +#~ msgstr "Ruso" + +#~ msgid "Russian verbs (imperfective + perfective)" +#~ msgstr "Verbos en ruso (imperfectivo+perfectivo)" + +#~ msgid "The file had no non-empty lines." +#~ msgstr "El archivo no tenía líneas vacías." + +#~ msgid "The file was not in UTF8 format." +#~ msgstr "El archivo no estaba en formato UTF8." + +#~ msgid "Untitled" +#~ msgstr "Sin título" + +#~ msgid "in %(a)s, on %(b)s" +#~ msgstr "en %(a)s, en %(b)s" diff --git a/anki/locale/libanki_fr_FR.po b/anki/locale/libanki_fr_FR.po new file mode 100644 index 000000000..fa1590e6f --- /dev/null +++ b/anki/locale/libanki_fr_FR.po @@ -0,0 +1,813 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: 2008-09-18 01:16+0100\n" +"Last-Translator: Emmanuel JARRI \n" +"Language-Team: LMS \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: French\n" +"X-Poedit-Country: FRANCE\n" +"X-Poedit-SourceCharset: utf-8\n" +"Plural-Forms: nplurals=2; plural=n>1;\n" + +#: deck.py:483 +#, python-format +msgid "" +"\n" +"

    Congratulations!

    You have finished the deck for now.

    \n" +"%(next)s\n" +"

    \n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
    \n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" +"\n" +"

    Félicitations!

    Vous avez fini le paquet pour aujourd'hui.

    \n" +"%(next)s\n" +"

    \n" +"- Il y a %(waiting)d\n" +" cartes \n" +"espacées.
    \n" +"- Il y a %(suspended)d\n" +" cartes \n" +"suspendues." + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" +"\n" +"Un format adapté à \"Remembering the Kanji\" de Heisig.\n" +"Vous êtes testé dans le sens mot-clé vers kanji.\n" +"\n" +"La disposition du test se fonde sur l'excellent travail du site \n" +"http://kanji.koohii.com/\n" +"\n" +"Le lien dans la question donne la liste des histoires proposées par\n" +"les utilisateurs. Vous devez vous enregistrer, c'est gratuit." + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" +"\n" +"Saisissez l'expression en français que vous souhaitez apprendre dans le champ 'Expression'.\n" +"Saisissez une description en japonais ou en français dans le champ 'Signification'." + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" +"\n" +"Le champ lecture est par défaut créé automatiquement,\n" +"et affiche la lecture de l'expression. Pour les mots normalement\n" +"écrits en hiragana ou en katakana et qui n'ont pas besoin\n" +"de lecture, vous pouvez mettre le mot dans le champ expression\n" +"et laisser vide le champ lecture. Une lecture ne sera pas \n" +"automatiquement créée pour les mots écrits seulement en\n" +"hiragana ou katakana.\n" +"\n" +"Notez bien que la création automatique de la signification n'est pas\n" +"parfaite, vous devriez donc la vérifier avant d'ajouter des cartes." + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "%(count)s %(gradename)s kanji." + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "%(gradename)s: %(count)s sur %(total)s (%(percent)0.1f%%)." + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "%0.1f secondes" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "il y a %s" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "%s jour" +msgstr[1] "%s jours" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "%s heure" +msgstr[1] "%s heures" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "%s minute" +msgstr[1] "%s minutes" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "%s mois" +msgstr[1] "%s mois" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "%s seconde" +msgstr[1] "%s secondes" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "%s année" +msgstr[1] "%s années" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, fuzzy, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "Cartes mûres : %(old)d (%(oldP)0.2f%%)
    " + +#: stats.py:367 +#, fuzzy, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "Ajouts : %(a)d par jour, %(b)d par mois
    " + +#: stats.py:356 +#, fuzzy, python-format +msgid "%0.0f days" +msgstr "Intervalle : %0.0f jours
    " + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, fuzzy, python-format +msgid "%0.1f cards/day" +msgstr "Charge de travail moyenne: %0.1f cartes par jour
    " + +#: stats.py:354 +msgid "Averages
    " +msgstr "Moyennes
    " + +#: stats.py:333 +msgid "Card counts
    " +msgstr "Décomptes de cartes
    " + +#: stats.py:341 +msgid "Correct answers
    " +msgstr "Réponses exactes
    " + +#: stats.py:522 +#, python-format +msgid "

    Kanji statistics

    The %d seen cards in this deck contain:" +msgstr "

    Statistiques relatives aux Kanji

    Les %d cartes vues de ce paquet contiennent :" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "
  • %d kanji différents.
  • " + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" +"Une fiche de révision simple avec un recto et un verso.\n" +"Les questions portent par défaut du recto au verso.\n" +"\n" +"Vous devriez personnaliser ce modèle plutôt que de l'utiliser\n" +"tel quel : des noms de champ comme \"expression\"\n" +"sont plus clairs que \"recto\" et \"verso\", et garantiront\n" +"que vos saisies sont cohérentes. " + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "Une description dans votre langue maternelle, ou en cantonais" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "Une description dans votre langue maternelle, ou en japonais" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "Une description dans votre langue maternelle, ou en mandarin" + +#: stdmodels.py:38 +msgid "A question." +msgstr "Une question." + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "Un mot ou expression écrit en Hanzi." + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "Un mot ou expression écrit en Kanji." + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "Testez activement votre souvenir en produisant l'expression cible" + +#: stats.py:256 +msgid "Added" +msgstr "Ajoutée" + +#: stats.py:373 +msgid "Added last month" +msgstr "" + +#: stats.py:370 +msgid "Added last week" +msgstr "" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "Paquet Anki (*.anki)" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "Paquets Anki (*.anki)" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" +"Au même moment demain :

    \n" +"- attendront %(wait)d cartes pour révision
    \n" +"- attendront %(new)d\n" +"\n" +"nouvelles cartes" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "Créé automatiquement par défaut" + +#: stats.py:357 +#, fuzzy +msgid "Average reps" +msgstr "Temps moyen" + +#: stats.py:277 +msgid "Average time" +msgstr "Temps moyen" + +#: stats.py:367 +#, fuzzy +msgid "Avg. added" +msgstr "Ajoutée" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "D'arrière en avant" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "Simple" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "Impossible de lire/écrire le paquet" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "Cantonais" + +#: stats.py:290 +msgid "Card model tags" +msgstr "Marqueurs de modèles de cartes" + +#: stats.py:289 +msgid "Card tags" +msgstr "Marqueurs de cartes" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +msgid "Cards" +msgstr "Cartes" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "Cartes dans un fichier texte à séparateur tabulation (*.txt)" + +#: models.py:25 +msgid "Center" +msgstr "Centre" + +#: stats.py:259 +msgid "Changed" +msgstr "Changée" + +#: stats.py:274 +msgid "Correct count" +msgstr "Nombre de fois correctes " + +#: stats.py:270 +msgid "Current factor" +msgstr "Facteur actuel" + +#: stats.py:266 +msgid "Current interval" +msgstr "Intervalle actuel" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "Base de données endommagée. Restauration depuis la sauvegarde." + +#: graphs.py:136 +msgid "Day" +msgstr "Jour" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "Jours" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "Statistiques de paquet" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "Paquet créé : il y a %s
    " + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "Il manque un modèle au paquet" + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "%d cartes sans faits supprimés" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "%d cartes sans modèles supprimées" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "%d champs en suspens" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "%d faits sans champs supprimés" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "%d faits sans cartes supprimés" + +#: stdmodels.py:100 +msgid "English" +msgstr "Anglais" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "Erreur en tentant d'exécuter 'latex' ou 'dvipng' - Sont-ils installés ?" + +#: stats.py:294 +msgid "Fact tags" +msgstr "Marqueurs de fait" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "Faits dans un fichier texte à séparateur tabulation (*.txt)" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "Un autre processus utilise ce fichier" + +#: stats.py:379 +#, fuzzy +msgid "First last month" +msgstr "Première fois" + +#: stats.py:376 +#, fuzzy +msgid "First last week" +msgstr "Première fois" + +#: stats.py:258 +msgid "First review" +msgstr "Première fois" + +#: stats.py:282 +msgid "First time" +msgstr "Première fois" + +#: stats.py:346 +#, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "Vues pour la première fois : %(gNewYes%)0.1f%% (%(gNewYes)d sur %(gNewTotal)d)

    " + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "Depuis l'expression en français vers la signification." + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "Du mot-clé au Kanji" + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "Depuis la signification vers l'expression en français." + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "D'avant en arrière" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "Heisig" + +#: stats.py:356 +#, fuzzy +msgid "Interval" +msgstr "Intervalle précédent" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "Japonais" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "Niveaux Jouyou" + +#: stats.py:271 +msgid "Last factor" +msgstr "Facteur précédent" + +#: stats.py:269 +msgid "Last interval" +msgstr "Intervalle précédent" + +#: models.py:26 +msgid "Left" +msgstr "Gauche" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "Mandarin" + +#: stats.py:286 +msgid "Mature" +msgstr "Mûre" + +#: graphs.py:210 +msgid "Mature cards" +msgstr "Cartes mûres" + +#: stats.py:342 +#, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "Cartes mûres : %(gMatureYes%)0.1f%% (%(gMatureYes)d sur %(gMatureTotal)d)
    " + +#: stats.py:334 +#, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "Cartes mûres : %(old)d (%(oldP)0.2f%%)
    " + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "Le modèle '%s' avait le mauvais nombre de cartes" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "Le modèle '%s' avait le mauvais nombre de champs" + +#: stats.py:292 +msgid "Model tags" +msgstr "Marqueurs de modèle" + +#: graphs.py:208 +msgid "New cards" +msgstr "Nouvelles cartes" + +#: stats.py:265 +msgid "Next due" +msgstr "Prochaine échéance" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "Veuillez d'abord ajouter des cartes.

    " + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "Répétitions correctes" + +#: stats.py:365 +#, fuzzy +msgid "Reps last month" +msgstr "%s mois" + +#: stats.py:363 +msgid "Reps last week" +msgstr "" + +#: stats.py:361 +#, fuzzy +msgid "Reps next month" +msgstr "%s mois" + +#: stats.py:359 +msgid "Reps next week" +msgstr "" + +#: stats.py:272 +msgid "Review count" +msgstr "Nombre de révisions" + +#: models.py:27 +msgid "Right" +msgstr "Droite" + +#: deck.py:1824 +#, fuzzy +msgid "Show new cards after all other cards" +msgstr "Montrer les nouvelles cartes dans l'ordre de leur ajout" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "Montrer les nouvelles cartes dans l'ordre de leur ajout" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "Montrer les nouvelles cartes au hasard" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "" + +#: stats.py:287 +msgid "State" +msgstr "État" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "Testez votre capacité à reconnaître l'expression cible" + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "Fichiers texte (*.txt)" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "La réponse." + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "Le modèle courant n'existait pas" + +#: deck.py:431 +#, fuzzy +msgid "The deck is empty. Please add some cards." +msgstr "Veuillez d'abord ajouter des cartes.

    " + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "La prochaine carte apparaîtra dans %s" + +#: stats.py:331 +#, python-format +msgid "Total number of cards: %d

    " +msgstr "Nombre total de cartes : %d

    " + +#: stats.py:279 +msgid "Total time" +msgstr "Temps total" + +#: deck.py:702 +msgid "Unknown" +msgstr "Inconnu" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "Cartes non vues: %(new)d (%(newP)0.2f%%)

    " + +#: stats.py:284 +msgid "Young" +msgstr "Récente" + +#: graphs.py:209 +msgid "Young cards" +msgstr "Cartes récentes" + +#: stats.py:344 +#, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "Cartes jeunes : %(gYoungYes%)0.1f%% (%(gYoungYes)d sur %(gYoungTotal)d)
    " + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "Cartes jeunes : %(young)d (%(youngP)0.2f%%)
    " + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "[vide]" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "[format invalide ; voir les propriétés du modèle]" + +#: deck.py:463 +msgid "a short time" +msgstr "un temps court" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "dans %s" + +#: deck.py:448 +msgid "unknown" +msgstr "inconnu" + +#~ msgid "" +#~ "\n" +#~ "Two cards: one tests meaning -> imperfective + perfective,\n" +#~ "and one tests imperfective -> perfective." +#~ msgstr "" +#~ "\n" +#~ "Deux cartes : l'une teste signification -> imperfectif + perfectif,\n" +#~ " et l'autre teste imperfectif -> perfectif." + +#~ msgid "" +#~ "\n" +#~ "Two cards: one tests production, one recognition." +#~ msgstr "" +#~ "\n" +#~ "Deux cartes: l'une teste la production, l'autre la reconnaissance." + +#~ msgid "%(a)d.%(b)df day" +#~ msgstr "%(a)d.%(b)df jour" + +#~ msgid "%(a)d.%(b)df hour" +#~ msgstr "%(a)d.%(b)df heure" + +#~ msgid "%(a)d.%(b)df minute" +#~ msgstr "%(a)d.%(b)df minute" + +#~ msgid "%(a)d.%(b)df second" +#~ msgstr "%(a)d.%(b)df seconde" + +#~ msgid "%(a)d.%(b)df year" +#~ msgstr "%(a)d.%(b)df année" + +#~ msgid "%s days" +#~ msgstr "%s jours" + +#~ msgid "%x at %H:%M" +#~ msgstr "%x à %H:%M" + +#~ msgid "A random position in the deck" +#~ msgstr "Une position aléatoire dans le paquet" + +#~ msgid "At the end of the deck" +#~ msgstr "À la fin du paquet" + +#~ msgid "At the start of the deck" +#~ msgstr "Au début du paquet" + +#~ msgid "Can't read/write directory" +#~ msgstr "Impossible de lire/écrire le répertoire" + +#~ msgid "Colon/semicolon/tab-separated text file (*.*)" +#~ msgstr "Fichier texte à séparateur virgule/point-virgule/tabulation (*.*)" + +#~ msgid "Couldn't find pattern. The file should be a series of lines separated by colons, semicolons or tabs." +#~ msgstr "Impossible de trouver le motif. Le fichier doit être une série de lignes séparées par des virgules, des points-virgules ou des tabulations." + +#~ msgid "Deck%" +#~ msgstr "Paquet%" + +#~ msgid "Fc .pending file (*.pending *.imported)" +#~ msgstr "Fichier Fc .pending (*.pending *.imported)" + +#~ msgid "From imperfective to perfective" +#~ msgstr "De l'imparfait au parfait" + +#~ msgid "From the Russian expression to the meaning." +#~ msgstr "Depuis l'expression en russe vers la signification." + +#~ msgid "From the meaning to an imperfective+perfective pair." +#~ msgstr "Depuis la signification vers un couple perfectif+ imperfectif" + +#~ msgid "From the meaning to the Russian expression." +#~ msgstr "Depuis la signification vers l'expression en russe." + +#~ msgid "Import: duplicate" +#~ msgstr "Importation : double" + +#~ msgid "Import: missing field" +#~ msgstr "Importation : champ manquant" + +#~ msgid "Import: same %s" +#~ msgstr "Importation : même %s" + +#~ msgid "Line %(line)d doesn't match pattern '%(pat)s'" +#~ msgstr "La ligne %(line)d ne correspond pas au modèle '%(pat)s'" + +#~ msgid "Line %(line)d had %(num1)d fields, expected %(num2)d" +#~ msgstr "La ligne %(line)d avait %(num1)d champs au lieu de %(num2)d prévus" + +#~ msgid "Mnemosyne file (*.mem)" +#~ msgstr "Fichier Mnemosyne (*.mem)" + +#~ msgid "Model" +#~ msgstr "Modèle" + +#~ msgid "Old Anki 0.2.x file (*.fc)" +#~ msgstr "Ancien fichier Anki 0.2.x (*.fc)" + +#~ msgid "Old cards" +#~ msgstr "Cartes anciennes" + +#~ msgid "Other" +#~ msgstr "Autre" + +#~ msgid "Period" +#~ msgstr "Période" + +#~ msgid "Russian" +#~ msgstr "russe" + +#~ msgid "Russian verbs (imperfective + perfective)" +#~ msgstr "Verbes russes (imparfait + parfait)" + +#~ msgid "The file had no non-empty lines." +#~ msgstr "Le fichier avait des lignes non vides." + +#~ msgid "The file was not in UTF8 format." +#~ msgstr "Le fichier n'était pas au format UTF-8" + +#~ msgid "Untitled" +#~ msgstr "Sans titre" + +#~ msgid "in %(a)s, on %(b)s" +#~ msgstr "dans %(a)s, sur %(b)s" diff --git a/anki/locale/libanki_ja_JP.po b/anki/locale/libanki_ja_JP.po new file mode 100644 index 000000000..020a32ab2 --- /dev/null +++ b/anki/locale/libanki_ja_JP.po @@ -0,0 +1,638 @@ +# Japanese translations for libanki +# Copyright (C) 2007 Damien Elmes +# This file is distributed under the same license as the libanki package. +# Damien Elmes , 2007. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Damien Elmes \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: deck.py:483 +#, python-format +msgid "" +"\n" +"

    Congratulations!

    You have finished the deck for now.

    \n" +"%(next)s\n" +"

    \n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
    \n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "" + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "" + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "%s前" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "%s日" +msgstr[1] "%s日" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "%s時間" +msgstr[1] "%s時間" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "%s分" +msgstr[1] "%s分" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "%sヶ月" +msgstr[1] "%sヶ月" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "%s秒" +msgstr[1] "%s秒" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "%s年" +msgstr[1] "%s年" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "" + +#: stats.py:367 +#, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "" + +#: stats.py:356 +#, python-format +msgid "%0.0f days" +msgstr "" + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, python-format +msgid "%0.1f cards/day" +msgstr "" + +#: stats.py:354 +msgid "Averages
    " +msgstr "" + +#: stats.py:333 +msgid "Card counts
    " +msgstr "" + +#: stats.py:341 +msgid "Correct answers
    " +msgstr "" + +#: stats.py:522 +#, python-format +msgid "

    Kanji statistics

    The %d seen cards in this deck contain:" +msgstr "" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "" + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "" + +#: stdmodels.py:38 +msgid "A question." +msgstr "" + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "" + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "" + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "" + +#: stats.py:256 +msgid "Added" +msgstr "" + +#: stats.py:373 +msgid "Added last month" +msgstr "" + +#: stats.py:370 +msgid "Added last week" +msgstr "" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "" + +#: stats.py:357 +msgid "Average reps" +msgstr "" + +#: stats.py:277 +msgid "Average time" +msgstr "" + +#: stats.py:367 +msgid "Avg. added" +msgstr "" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "裏側から表側" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "" + +#: stats.py:290 +msgid "Card model tags" +msgstr "" + +#: stats.py:289 +msgid "Card tags" +msgstr "" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +msgid "Cards" +msgstr "" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "" + +#: models.py:25 +msgid "Center" +msgstr "" + +#: stats.py:259 +msgid "Changed" +msgstr "" + +#: stats.py:274 +msgid "Correct count" +msgstr "" + +#: stats.py:270 +msgid "Current factor" +msgstr "" + +#: stats.py:266 +msgid "Current interval" +msgstr "" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "" + +#: graphs.py:136 +msgid "Day" +msgstr "" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "" + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "" + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "" + +#: stdmodels.py:100 +msgid "English" +msgstr "" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "" + +#: stats.py:294 +msgid "Fact tags" +msgstr "" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "" + +#: stats.py:379 +msgid "First last month" +msgstr "" + +#: stats.py:376 +msgid "First last week" +msgstr "" + +#: stats.py:258 +msgid "First review" +msgstr "" + +#: stats.py:282 +msgid "First time" +msgstr "" + +#: stats.py:346 +#, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "" + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "" + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "" + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "" + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "表側から裏側" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "" + +#: stats.py:356 +msgid "Interval" +msgstr "" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "" + +#: stats.py:271 +msgid "Last factor" +msgstr "" + +#: stats.py:269 +msgid "Last interval" +msgstr "" + +#: models.py:26 +msgid "Left" +msgstr "" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "" + +#: stats.py:286 +msgid "Mature" +msgstr "" + +#: graphs.py:210 +msgid "Mature cards" +msgstr "" + +#: stats.py:342 +#, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "" + +#: stats.py:334 +#, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "" + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "" + +#: stats.py:292 +msgid "Model tags" +msgstr "" + +#: graphs.py:208 +msgid "New cards" +msgstr "" + +#: stats.py:265 +msgid "Next due" +msgstr "" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "" + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "" + +#: stats.py:365 +#, fuzzy +msgid "Reps last month" +msgstr "%sヶ月" + +#: stats.py:363 +msgid "Reps last week" +msgstr "" + +#: stats.py:361 +#, fuzzy +msgid "Reps next month" +msgstr "%sヶ月" + +#: stats.py:359 +msgid "Reps next week" +msgstr "" + +#: stats.py:272 +msgid "Review count" +msgstr "" + +#: models.py:27 +msgid "Right" +msgstr "" + +#: deck.py:1824 +msgid "Show new cards after all other cards" +msgstr "" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "" + +#: stats.py:287 +msgid "State" +msgstr "" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "" + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "" + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "" + +#: deck.py:431 +msgid "The deck is empty. Please add some cards." +msgstr "" + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "" + +#: stats.py:331 +#, python-format +msgid "Total number of cards: %d

    " +msgstr "" + +#: stats.py:279 +msgid "Total time" +msgstr "" + +#: deck.py:702 +msgid "Unknown" +msgstr "" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "" + +#: stats.py:284 +msgid "Young" +msgstr "" + +#: graphs.py:209 +msgid "Young cards" +msgstr "" + +#: stats.py:344 +#, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "" + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "" + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "" + +#: deck.py:463 +msgid "a short time" +msgstr "何分" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "" + +#: deck.py:448 +msgid "unknown" +msgstr "" diff --git a/anki/locale/libanki_ko_KR.po b/anki/locale/libanki_ko_KR.po new file mode 100644 index 000000000..823212556 --- /dev/null +++ b/anki/locale/libanki_ko_KR.po @@ -0,0 +1,677 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: 2008-05-24 09:18+0900\n" +"Last-Translator: Jin Eun-Deok \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: deck.py:483 +#, python-format +msgid "" +"\n" +"

    Congratulations!

    You have finished the deck for now.

    \n" +"%(next)s\n" +"

    \n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
    \n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" +"\n" +"Heisig의 일본어 한자 교재 \"Remembering the Kanji\"에 적합한 모델.\n" +"뜻을 보고 해당 한자를 맞춥니다.\n" +"\n" +"질문 형식은 http://kanji.koohii.com/을 바탕으로 했습니다.\n" +"\n" +"질문에 표시되는 링크는 회원들이 제공한 해설 페이지로 연결됩니다.\n" +"해당 사이트로 로그인을 해야 합니다." + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" +"\n" +"공부하고 싶은 영어 표현을 'Expression' 필드에 입력합니다.\n" +"영어 표현의 뜻을 한국어나 영어로 'Meaning' 필드에 입력합니다." + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" +"\n" +"reading(독음) 필드는 기본적으로 자동 입력되어\n" +"expression(표현) 필드의 일본어를 읽는 법을 보여줍니다.\n" +"히라카나와 카타카나로 이루어진 표현은 따로 독음이 필요없습니다.\n" +"따라서 expression 필드만 입력하고 reading 필드는 비워 둬도 됩니다.\n" +"히라카나와 카타카나로만 구성된 표현에 대해서는\n" +"자동으로 독음을 만들지 않습니다.\n" +"\n" +"자동 생성된 독음은 틀릴 수 있습니다.\n" +"카드를 추가하기 전에 다시 확인해 주세요." + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "%(count)s %(gradename)s kanji." + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "%(gradename)s: %(total)s자 중에 %(count)s자 (%(percent)0.1f%%)." + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "%0.1f초" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "%s전" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "%s일" +msgstr[1] "%s일" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "%s시간" +msgstr[1] "%s시간" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "%s분" +msgstr[1] "%s분" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "%s개월" +msgstr[1] "%s개월" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "%s초" +msgstr[1] "%s초" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "%s년" +msgstr[1] "%s년" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, fuzzy, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "성숙한 카드: %(old)d (%(oldP)0.2f%%)
    " + +#: stats.py:367 +#, fuzzy, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "카드 추가: 하루에 %(a)d개, 한달에 %(b)d
    " + +#: stats.py:356 +#, fuzzy, python-format +msgid "%0.0f days" +msgstr "복습 간격: %0.0f
    " + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, fuzzy, python-format +msgid "%0.1f cards/day" +msgstr "학습량: %0.1f 카드/일
    " + +#: stats.py:354 +msgid "Averages
    " +msgstr "평균
    " + +#: stats.py:333 +msgid "Card counts
    " +msgstr "카드 개수
    " + +#: stats.py:341 +msgid "Correct answers
    " +msgstr "정답률
    " + +#: stats.py:522 +#, python-format +msgid "

    Kanji statistics

    The %d seen cards in this deck contain:" +msgstr "

    일본 한자 통계

    현재 묶음에서 지금까지 본 %d개의 카드 중:" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "
  • 전체 한자 %d자
  • " + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" +"앞면과 뒷면으로 이루어진 단순한 플래시 카드\n" +"기본적으로 앞면을 질문합니다.\n" +"\n" +"이 모델을 그대로 쓰기보다는 상황에 맞게\n" +"수정해서 사용하세요. 예를 들어 \"expression(표현)\"이라는 필드 이름이\n" +"\"front(앞면)\"이나 \"back(뒷면)\"보다 필드에 들어갈 내용을\n" +"분명하게 알려 줍니다." + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "한국어나 광동어 해설" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "한국어나 일본어 해설" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "한국어나 북경어 해설" + +#: stdmodels.py:38 +msgid "A question." +msgstr "질문." + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "중국 한자로 쓴 단어나 표현" + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "일본 한자로 쓴 단어나 표현" + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "해당 표현을 만들어 보면서 능동적으로 기억 해보세요" + +#: stats.py:256 +msgid "Added" +msgstr "추가한 날" + +#: stats.py:373 +msgid "Added last month" +msgstr "" + +#: stats.py:370 +msgid "Added last week" +msgstr "" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "앙키 묶음 (*.anki)" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "앙키 묶음 (*.anki)" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "자동으로 생성됨" + +#: stats.py:357 +#, fuzzy +msgid "Average reps" +msgstr "평균 시간" + +#: stats.py:277 +msgid "Average time" +msgstr "평균 시간" + +#: stats.py:367 +#, fuzzy +msgid "Avg. added" +msgstr "추가한 날" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "뒷면에서 앞면으로" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "기본" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "묶음을 읽고 쓸 수 없음" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "광동어" + +#: stats.py:290 +msgid "Card model tags" +msgstr "카드 모델 꼬리표" + +#: stats.py:289 +msgid "Card tags" +msgstr "카드 꼬리표" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +#, fuzzy +msgid "Cards" +msgstr "카드 꼬리표" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "탭으로 구분한 카드 텍스트 파일 (*.txt)" + +#: models.py:25 +msgid "Center" +msgstr "가운데" + +#: stats.py:259 +msgid "Changed" +msgstr "변경" + +#: stats.py:274 +msgid "Correct count" +msgstr "맞춘 횟수" + +#: stats.py:270 +msgid "Current factor" +msgstr "현재 인수" + +#: stats.py:266 +msgid "Current interval" +msgstr "현재 복습 간격" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "" + +#: graphs.py:136 +msgid "Day" +msgstr "" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "묶음 통계" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "묶음 생성: %s
    " + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "" + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "" + +#: stdmodels.py:100 +msgid "English" +msgstr "영어" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "" + +#: stats.py:294 +msgid "Fact tags" +msgstr "지식 꼬리표" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "탭으로 구분한 지식 텍스트 파일 (*.txt)" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "다른 프로세스가 파일을 사용하고 있습니다." + +#: stats.py:379 +#, fuzzy +msgid "First last month" +msgstr "처음" + +#: stats.py:376 +#, fuzzy +msgid "First last week" +msgstr "처음" + +#: stats.py:258 +msgid "First review" +msgstr "최초 복습" + +#: stats.py:282 +msgid "First time" +msgstr "처음" + +#: stats.py:346 +#, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "처음 본 카드: %(gNewYes%)0.1f%% (%(gNewYes)d개 중에 %(gNewTotal)d개)

    " + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "영어 표현에서 뜻으로" + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "중심 의미에서 한자로" + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "뜻에서 영어 표현으로" + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "앞면에서 뒷면으로" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "Heisig" + +#: stats.py:356 +#, fuzzy +msgid "Interval" +msgstr "지난 복습 간격" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "일본어" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "상용한자 수준" + +#: stats.py:271 +msgid "Last factor" +msgstr "지난 인수" + +#: stats.py:269 +msgid "Last interval" +msgstr "지난 복습 간격" + +#: models.py:26 +msgid "Left" +msgstr "왼쪽" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "북경어" + +#: stats.py:286 +msgid "Mature" +msgstr "성숙한 카드" + +#: graphs.py:210 +msgid "Mature cards" +msgstr "성숙한 카드" + +#: stats.py:342 +#, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "성숙한 카드: %(gMatureYes%)0.1f%% (%(gMatureTotal)d개 중에 %(gMatureYes)d개)
    " + +#: stats.py:334 +#, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "성숙한 카드: %(old)d (%(oldP)0.2f%%)
    " + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "" + +#: stats.py:292 +msgid "Model tags" +msgstr "모델 꼬리표" + +#: graphs.py:208 +msgid "New cards" +msgstr "새 카드" + +#: stats.py:265 +msgid "Next due" +msgstr "복습 예정" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "카드를 먼저 추가해 주세요.

    " + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "연속 정답" + +#: stats.py:365 +#, fuzzy +msgid "Reps last month" +msgstr "%s개월" + +#: stats.py:363 +msgid "Reps last week" +msgstr "" + +#: stats.py:361 +#, fuzzy +msgid "Reps next month" +msgstr "%s개월" + +#: stats.py:359 +msgid "Reps next week" +msgstr "" + +#: stats.py:272 +msgid "Review count" +msgstr "복습 횟수" + +#: models.py:27 +msgid "Right" +msgstr "오른쪽" + +#: deck.py:1824 +#, fuzzy +msgid "Show new cards after all other cards" +msgstr "추가된 순서대로 보여주기" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "추가된 순서대로 보여주기" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "무작위로 보여주기" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "" + +#: stats.py:287 +msgid "State" +msgstr "상태" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "해당 표현을 이해할 수 있는지 시험해 보세요." + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "텍스트 파일 (*.txt)" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "답." + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "" + +#: deck.py:431 +#, fuzzy +msgid "The deck is empty. Please add some cards." +msgstr "카드를 먼저 추가해 주세요.

    " + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "" + +#: stats.py:331 +#, python-format +msgid "Total number of cards: %d

    " +msgstr "전체 카드 개수: %d

    " + +#: stats.py:279 +msgid "Total time" +msgstr "전체 시간" + +#: deck.py:702 +msgid "Unknown" +msgstr "알 수 없음" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "시작하지 않은 카드: %(new)d (%(newP)0.2f%%)

    " + +#: stats.py:284 +msgid "Young" +msgstr "어린 카드" + +#: graphs.py:209 +msgid "Young cards" +msgstr "어린 카드" + +#: stats.py:344 +#, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "어린 카드: %(gYoungYes%)0.1f%% (%(gYoungTotal)d개 중에 %(gYoungYes)d개)
    " + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "어린 카드: %(young)d (%(youngP)0.2f%%)
    " + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "[자료 없음]" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "[잘못된 형식: 모델 속성 참조]" + +#: deck.py:463 +msgid "a short time" +msgstr "짧은 시간" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "%s 안에" + +#: deck.py:448 +msgid "unknown" +msgstr "알 수 없음" + +#~ msgid "Can't read/write directory" +#~ msgstr "디렉토리를 읽고 쓸 수 없음" diff --git a/anki/locale/messages.pot b/anki/locale/messages.pot new file mode 100644 index 000000000..72c838016 --- /dev/null +++ b/anki/locale/messages.pot @@ -0,0 +1,638 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-09-23 03:21+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: deck.py:483 +#, python-format +msgid "" +"\n" +"

    Congratulations!

    You have finished the deck for now.

    \n" +"%(next)s\n" +"

    \n" +"- There are %(waiting)d\n" +"\n" +"spaced cards.
    \n" +"- There are %(suspended)d\n" +"\n" +"suspended cards." +msgstr "" + +#: stdmodels.py:121 +msgid "" +"\n" +"A format suitable for Heisig's \"Remembering the Kanji\".\n" +"You are tested from the keyword to the kanji.\n" +"\n" +"Layout of the test is based on the great work at\n" +"http://kanji.koohii.com/\n" +"\n" +"The link in the question will list user-contributed\n" +"stories. A free login is required." +msgstr "" + +#: stdmodels.py:101 +msgid "" +"\n" +"Enter the English expression you want to learn in the 'Expression' field.\n" +"Enter a description in Japanese or English in the 'Meaning' field." +msgstr "" + +#: stdmodels.py:52 +msgid "" +"\n" +"The reading field is automatically generated by default,\n" +"and shows the reading for the expression. For words that\n" +"are normally written in hiragana or katakana and don't\n" +"need a reading, you can put the word in the expression\n" +"field, and leave the reading field blank. A reading will\n" +"will not automatically be generated for words written\n" +"in only hiragana or katakana.\n" +"\n" +"Note that the automatic generation of meaning is not\n" +"perfect, and should be checked before adding cards." +msgstr "" + +#: stats.py:500 +#, python-format +msgid "%(count)s %(gradename)s kanji." +msgstr "" + +#: stats.py:498 +#, python-format +msgid "%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%)." +msgstr "" + +#: stats.py:277 stats.py:279 +#, python-format +msgid "%0.1f seconds" +msgstr "" + +#: stats.py:262 stats.py:303 +#, python-format +msgid "%s ago" +msgstr "" + +#: utils.py:19 +#, python-format +msgid "%s day" +msgid_plural "%s days" +msgstr[0] "" +msgstr[1] "" + +#: utils.py:20 +#, python-format +msgid "%s hour" +msgid_plural "%s hours" +msgstr[0] "" +msgstr[1] "" + +#: utils.py:21 +#, python-format +msgid "%s minute" +msgid_plural "%s minutes" +msgstr[0] "" +msgstr[1] "" + +#: utils.py:18 +#, python-format +msgid "%s month" +msgid_plural "%s months" +msgstr[0] "" +msgstr[1] "" + +#: utils.py:22 +#, python-format +msgid "%s second" +msgid_plural "%s seconds" +msgstr[0] "" +msgstr[1] "" + +#: utils.py:17 +#, python-format +msgid "%s year" +msgid_plural "%s years" +msgstr[0] "" +msgstr[1] "" + +#: stats.py:370 stats.py:373 stats.py:376 stats.py:379 +#, python-format +msgid "%(a)d (%(b)0.1f/day)" +msgstr "" + +#: stats.py:367 +#, python-format +msgid "%(a)d/day, %(b)d/mon" +msgstr "" + +#: stats.py:356 +#, python-format +msgid "%0.0f days" +msgstr "" + +#: stats.py:357 stats.py:359 stats.py:361 stats.py:363 stats.py:365 +#, python-format +msgid "%0.1f cards/day" +msgstr "" + +#: stats.py:354 +msgid "Averages
    " +msgstr "" + +#: stats.py:333 +msgid "Card counts
    " +msgstr "" + +#: stats.py:341 +msgid "Correct answers
    " +msgstr "" + +#: stats.py:522 +#, python-format +msgid "

    Kanji statistics

    The %d seen cards in this deck contain:" +msgstr "" + +#: stats.py:526 +#, python-format +msgid "
  • %d total unique kanji.
  • " +msgstr "" + +#: sync.py:171 +#, python-format +msgid "" +"\n" +"\n" +"\n" +"\n" +"\n" +"%(media)s\n" +"
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    " +msgstr "" + +#: stdmodels.py:32 +msgid "" +"A basic flashcard with a front and a back.\n" +"Questions are asked from front to back by default.\n" +"\n" +"Please consider customizing this model, rather than\n" +"using it verbatim: field names like \"expression\" are\n" +"clearer than \"front\" and \"back\", and will ensure\n" +"that your entries are consistent." +msgstr "" + +#: stdmodels.py:164 +msgid "A description in your native language, or Cantonese" +msgstr "" + +#: stdmodels.py:75 +msgid "A description in your native language, or Japanese" +msgstr "" + +#: stdmodels.py:192 +msgid "A description in your native language, or Mandarin" +msgstr "" + +#: stdmodels.py:38 +msgid "A question." +msgstr "" + +#: stdmodels.py:159 stdmodels.py:186 +msgid "A word or expression written in Hanzi." +msgstr "" + +#: stdmodels.py:65 +msgid "A word or expression written in Kanji." +msgstr "" + +#: stdmodels.py:84 stdmodels.py:170 stdmodels.py:199 +msgid "Actively test your recall by producing the target expression" +msgstr "" + +#: stats.py:256 +msgid "Added" +msgstr "" + +#: stats.py:373 +msgid "Added last month" +msgstr "" + +#: stats.py:370 +msgid "Added last week" +msgstr "" + +#: exporting.py:216 +msgid "Anki deck (*.anki)" +msgstr "" + +#: exporting.py:55 +msgid "Anki decks (*.anki)" +msgstr "" + +#: deck.py:418 +#, python-format +msgid "" +"At the same time tomorrow:

    \n" +"- There will be %(wait)d cards waiting for review
    \n" +"- There will be %(new)d\n" +"\n" +"new cards waiting" +msgstr "" + +#: stdmodels.py:78 stdmodels.py:166 stdmodels.py:194 +msgid "Automatically generated by default." +msgstr "" + +#: stats.py:357 +msgid "Average reps" +msgstr "" + +#: stats.py:277 +msgid "Average time" +msgstr "" + +#: stats.py:367 +msgid "Avg. added" +msgstr "" + +#: stdmodels.py:42 +msgid "Back to front" +msgstr "" + +#: stdmodels.py:31 +msgid "Basic" +msgstr "" + +#: deck.py:1452 deck.py:1525 +msgid "Can't read/write deck" +msgstr "" + +#: stdmodels.py:156 +msgid "Cantonese" +msgstr "" + +#: stats.py:290 +msgid "Card model tags" +msgstr "" + +#: stats.py:289 +msgid "Card tags" +msgstr "" + +#: graphs.py:77 graphs.py:99 graphs.py:113 graphs.py:135 +msgid "Cards" +msgstr "" + +#: exporting.py:217 +msgid "Cards in tab-separated text file (*.txt)" +msgstr "" + +#: models.py:25 +msgid "Center" +msgstr "" + +#: stats.py:259 +msgid "Changed" +msgstr "" + +#: stats.py:274 +msgid "Correct count" +msgstr "" + +#: stats.py:270 +msgid "Current factor" +msgstr "" + +#: stats.py:266 +msgid "Current interval" +msgstr "" + +#: deck.py:1311 +msgid "Database file damaged. Restore from backup." +msgstr "" + +#: graphs.py:136 +msgid "Day" +msgstr "" + +#: graphs.py:78 graphs.py:100 graphs.py:114 +msgid "Days" +msgstr "" + +#: stats.py:318 +msgid "Deck Statistics" +msgstr "" + +#: stats.py:319 +#, python-format +msgid "Deck created: %s ago
    " +msgstr "" + +#: deck.py:1319 +msgid "Deck was missing a model" +msgstr "" + +#: deck.py:1340 +#, python-format +msgid "Deleted %d cards with missing fact" +msgstr "" + +#: deck.py:1348 +#, python-format +msgid "Deleted %d cards with no card model" +msgstr "" + +#: deck.py:1361 +#, python-format +msgid "Deleted %d dangling fields" +msgstr "" + +#: deck.py:1333 +#, python-format +msgid "Deleted %d facts with missing fields" +msgstr "" + +#: deck.py:1353 +#, python-format +msgid "Deleted %d facts with no cards" +msgstr "" + +#: stdmodels.py:100 +msgid "English" +msgstr "" + +#: latex.py:104 +msgid "Error executing 'latex' or 'dvipng' - are they installed?" +msgstr "" + +#: stats.py:294 +msgid "Fact tags" +msgstr "" + +#: exporting.py:218 +msgid "Facts in tab-separated text file (*.txt)" +msgstr "" + +#: deck.py:1497 +msgid "File is in use by another process" +msgstr "" + +#: stats.py:379 +msgid "First last month" +msgstr "" + +#: stats.py:376 +msgid "First last week" +msgstr "" + +#: stats.py:258 +msgid "First review" +msgstr "" + +#: stats.py:282 +msgid "First time" +msgstr "" + +#: stats.py:346 +#, python-format +msgid "First-seen cards: %(gNewYes%)0.1f%% (%(gNewYes)d of %(gNewTotal)d)

    " +msgstr "" + +#: stdmodels.py:110 +msgid "From the English expression to the meaning." +msgstr "" + +#: stdmodels.py:142 +msgid "From the keyword to the Kanji." +msgstr "" + +#: stdmodels.py:107 +msgid "From the meaning to the English expression." +msgstr "" + +#: stdmodels.py:40 +msgid "Front to back" +msgstr "" + +#: stdmodels.py:120 +msgid "Heisig" +msgstr "" + +#: stats.py:356 +msgid "Interval" +msgstr "" + +#: stdmodels.py:51 +msgid "Japanese" +msgstr "" + +#: stats.py:537 +msgid "Jouyou levels:" +msgstr "" + +#: stats.py:271 +msgid "Last factor" +msgstr "" + +#: stats.py:269 +msgid "Last interval" +msgstr "" + +#: models.py:26 +msgid "Left" +msgstr "" + +#: stdmodels.py:183 +msgid "Mandarin" +msgstr "" + +#: stats.py:286 +msgid "Mature" +msgstr "" + +#: graphs.py:210 +msgid "Mature cards" +msgstr "" + +#: stats.py:342 +#, python-format +msgid "Mature cards: %(gMatureYes%)0.1f%% (%(gMatureYes)d of %(gMatureTotal)d)
    " +msgstr "" + +#: stats.py:334 +#, python-format +msgid "Mature cards: %(old)d (%(oldP)0.2f%%)
    " +msgstr "" + +#: media.py:161 media.py:163 media.py:164 +msgid "Media Missing" +msgstr "" + +#: deck.py:915 +#, python-format +msgid "Model '%s' has wrong card model count" +msgstr "" + +#: deck.py:919 +#, python-format +msgid "Model '%s' has wrong field model count" +msgstr "" + +#: stats.py:292 +msgid "Model tags" +msgstr "" + +#: graphs.py:208 +msgid "New cards" +msgstr "" + +#: stats.py:265 +msgid "Next due" +msgstr "" + +#: stats.py:316 +msgid "Please add some cards first.

    " +msgstr "" + +#: stats.py:276 +msgid "Repeatedly correct" +msgstr "" + +#: stats.py:365 +msgid "Reps last month" +msgstr "" + +#: stats.py:363 +msgid "Reps last week" +msgstr "" + +#: stats.py:361 +msgid "Reps next month" +msgstr "" + +#: stats.py:359 +msgid "Reps next week" +msgstr "" + +#: stats.py:272 +msgid "Review count" +msgstr "" + +#: models.py:27 +msgid "Right" +msgstr "" + +#: deck.py:1824 +msgid "Show new cards after all other cards" +msgstr "" + +#: deck.py:1818 +msgid "Show new cards in order they were added" +msgstr "" + +#: deck.py:1817 +msgid "Show new cards in random order" +msgstr "" + +#: deck.py:1823 +msgid "Spread new cards out through reviews" +msgstr "" + +#: stats.py:287 +msgid "State" +msgstr "" + +#: stdmodels.py:88 stdmodels.py:174 stdmodels.py:203 +msgid "Test your ability to recognize the target expression" +msgstr "" + +#: exporting.py:145 exporting.py:177 +msgid "Text files (*.txt)" +msgstr "" + +#: stdmodels.py:39 +msgid "The answer." +msgstr "" + +#: deck.py:1325 +msgid "The current model didn't exist" +msgstr "" + +#: deck.py:431 +msgid "The deck is empty. Please add some cards." +msgstr "" + +#: deck.py:428 +#, python-format +msgid "The next card will be shown in %s" +msgstr "" + +#: stats.py:331 +#, python-format +msgid "Total number of cards: %d

    " +msgstr "" + +#: stats.py:279 +msgid "Total time" +msgstr "" + +#: deck.py:702 +msgid "Unknown" +msgstr "" + +#: stats.py:338 +#, python-format +msgid "Unseen cards: %(new)d (%(newP)0.2f%%)

    " +msgstr "" + +#: stats.py:284 +msgid "Young" +msgstr "" + +#: graphs.py:209 +msgid "Young cards" +msgstr "" + +#: stats.py:344 +#, python-format +msgid "Young cards: %(gYoungYes%)0.1f%% (%(gYoungYes)d of %(gYoungTotal)d)
    " +msgstr "" + +#: stats.py:336 +#, python-format +msgid "Young cards: %(young)d (%(youngP)0.2f%%)
    " +msgstr "" + +#: models.py:150 models.py:151 models.py:182 +msgid "[empty]" +msgstr "" + +#: models.py:148 +msgid "[invalid format; see model properties]" +msgstr "" + +#: deck.py:463 +msgid "a short time" +msgstr "" + +#: stats.py:264 +#, python-format +msgid "in %s" +msgstr "" + +#: deck.py:448 +msgid "unknown" +msgstr "" diff --git a/anki/media.py b/anki/media.py new file mode 100644 index 000000000..99d5d2c69 --- /dev/null +++ b/anki/media.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Media support +==================== +""" +__docformat__ = 'restructuredtext' + +try: + import hashlib + md5 = hashlib.md5 +except ImportError: + import md5 + md5 = md5.new + +import os, stat, time, shutil, re +from anki.db import * +from anki.facts import Fact +from anki.utils import addTags, genID, ids2str +from anki.lang import _ + +regexps = (("(\[sound:([^]]+)\])", + "[sound:%s]"), + ("(]+)[\"']? ?/?>)", + "")) + +# Tables +########################################################################## + +mediaTable = Table( + 'media', metadata, + Column('id', Integer, primary_key=True, nullable=False), + Column('filename', UnicodeText, nullable=False), + Column('size', Integer, nullable=False), + Column('created', Float, nullable=False), + Column('originalPath', UnicodeText, nullable=False, default=u""), + Column('description', UnicodeText, nullable=False, default=u"")) + +class Media(object): + pass + +mapper(Media, mediaTable) + +mediaDeletedTable = Table( + 'mediaDeleted', metadata, + Column('mediaId', Integer, ForeignKey("cards.id"), + nullable=False), + Column('deletedTime', Float, nullable=False)) + +# Helper functions +########################################################################## + +def checksum(data): + return md5(data).hexdigest() + +def mediaFilename(path): + "Return checksum.ext for path" + new = checksum(open(path, "rb").read()) + ext = os.path.splitext(path)[1] + return "%s%s" % (new, ext) + +def copyToMedia(deck, path): + """Copy PATH to MEDIADIR, and return new filename. +Update media table. If file already exists, don't copy.""" + newBase = mediaFilename(path) + new = os.path.join(deck.mediaDir(create=True), newBase) + # copy if not existing + if not os.path.exists(new): + shutil.copy2(path, new) + newSize = os.stat(new)[stat.ST_SIZE] + if not deck.s.scalar( + "select 1 from media where filename = :f", + f=newBase): + deck.s.statement(""" +insert into media (id, filename, size, created, originalPath, +description) +values (:id, :filename, :size, :created, :originalPath, +:description)""", + id=genID(), + filename=newBase, + size=newSize, + created=time.time(), + originalPath=path, + description=os.path.splitext( + os.path.basename(path))[0]) + return newBase + +def _modifyFields(deck, fieldsToUpdate, modifiedFacts, dirty): + factIds = ids2str(modifiedFacts.keys()) + if fieldsToUpdate: + deck.s.execute("update fields set value = :val where id = :id", + fieldsToUpdate) + deck.s.statement( + "update facts set modified = :time where id in %s" % + factIds, time=time.time()) + ids = deck.s.all("""select cards.id, cards.cardModelId, facts.id +from cards, facts where cards.factId = facts.id and facts.id in %s""" + % factIds) + deck.updateCardQACache(ids, dirty) + deck.flushMod() + +def rebuildMediaDir(deck, deleteRefs=False, dirty=True): + "Delete references to missing files, delete unused files." + localFiles = {} + modifiedFacts = {} + unmodifiedFacts = {} + renamedFiles = {} + existingFiles = {} + factsMissingMedia = {} + updateFields = [] + usedFiles = {} + unusedFileCount = 0 + missingFileCount = 0 + deck.mediaDir(create=True) + # rename all files to checksum versions, note non-renamed ones + for oldBase in os.listdir(unicode(deck.mediaDir())): + oldPath = os.path.join(deck.mediaDir(), oldBase) + if oldBase.startswith("."): + continue + if oldBase.startswith("latex-"): + continue + if os.path.isdir(oldPath): + continue + newBase = copyToMedia(deck, oldPath) + if oldBase == newBase: + existingFiles[oldBase] = 1 + else: + renamedFiles[oldBase] = newBase + # now look through all fields, and update references to files + for (id, fid, val) in deck.s.all( + "select id, factId, value from fields"): + oldval = val + for (full, fname, repl) in mediaRefs(val): + if fname in renamedFiles: + # renamed + newBase = renamedFiles[fname] + val = re.sub(re.escape(full), repl % newBase, val) + usedFiles[newBase] = 1 + elif fname in existingFiles: + # used & current + usedFiles[fname] = 1 + else: + # missing + missingFileCount += 1 + if deleteRefs: + val = re.sub(re.escape(full), "", val) + else: + factsMissingMedia[fid] = 1 + if val != oldval: + updateFields.append({'id': id, 'val': val}) + modifiedFacts[fid] = 1 + else: + unmodifiedFacts[fid] = 1 + # update modified fields + if modifiedFacts: + _modifyFields(deck, updateFields, modifiedFacts, dirty) + # fix tags + if dirty: + if deleteRefs: + deck.deleteFactTags(modifiedFacts.keys(), _("Media Missing")) + else: + deck.addFactTags(modifiedFacts.keys(), _("Media Missing")) + deck.deleteFactTags(unmodifiedFacts.keys(), _("Media Missing")) + # build cache of db records + mediaIds = dict(deck.s.all("select filename, id from media")) + # look through the media dir for any unused files, and delete + for f in os.listdir(unicode(deck.mediaDir())): + if f.startswith("."): + continue + if f.startswith("latex-"): + continue + path = os.path.join(deck.mediaDir(), f) + if os.path.isdir(path): + shutil.rmtree(path) + continue + if f in usedFiles: + del mediaIds[f] + else: + os.unlink(path) + unusedFileCount += 1 + for (fname, id) in mediaIds.items(): + # maybe delete from db + if id: + deck.s.statement("delete from media where id = :id", id=id) + deck.s.statement(""" +insert into mediaDeleted (mediaId, deletedTime) +values (:id, strftime('%s', 'now'))""", id=id) + # update deck and save + deck.flushMod() + deck.save() + return missingFileCount, unusedFileCount + +def mediaRefs(string): + "Return list of (fullMatch, filename, replacementString)." + l = [] + for (reg, repl) in regexps: + for (full, fname) in re.findall(reg, string): + l.append((full, fname, repl)) + return l diff --git a/anki/models.py b/anki/models.py new file mode 100644 index 000000000..9a8652f3b --- /dev/null +++ b/anki/models.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Model - define the way in which facts are added and shown +========================================================== + +- Field models +- Card models +- Models + +""" + +import time +from sqlalchemy.ext.orderinglist import ordering_list +from anki.db import * +from anki.utils import genID +from anki.fonts import toPlatformFont +from anki.utils import parseTags +from anki.lang import _ + +def alignmentLabels(): + return { + 0: _("Center"), + 1: _("Left"), + 2: _("Right"), + } + +# Field models +########################################################################## + +fieldModelsTable = Table( + 'fieldModels', metadata, + Column('id', Integer, primary_key=True), + Column('ordinal', Integer, nullable=False), + Column('modelId', Integer, ForeignKey('models.id'), nullable=False), + Column('name', UnicodeText, nullable=False), + Column('description', UnicodeText, nullable=False, default=u""), + Column('features', UnicodeText, nullable=False, default=u""), + Column('required', Boolean, nullable=False, default=True), + Column('unique', Boolean, nullable=False, default=True), + Column('numeric', Boolean, nullable=False, default=False), + # display + Column('quizFontFamily', UnicodeText), + Column('quizFontSize', Integer), + Column('quizFontColour', String(7)), + Column('editFontFamily', UnicodeText), + Column('editFontSize', Integer, default=20)) + +class FieldModel(object): + "The definition of one field in a fact." + + def __init__(self, name=u"", description=u"", required=True, unique=True): + self.name = name + self.description = description + self.required = required + self.unique = unique + self.id = genID() + + def css(self, type="quiz"): + t = ".%s { " % self.name.replace(" ", "") + if getattr(self, type+'FontFamily'): + t += "font-family: \"%s\"; " % toPlatformFont( + getattr(self, type+'FontFamily')) + if getattr(self, type+'FontSize'): + t += "font-size: %dpx; " % getattr(self, type+'FontSize') + if type == "quiz" and getattr(self, type+'FontColour'): + t += "color: %s; " % getattr(self, type+'FontColour') + t += " }\n" + return t + +mapper(FieldModel, fieldModelsTable) + +# Card models +########################################################################## + +cardModelsTable = Table( + 'cardModels', metadata, + Column('id', Integer, primary_key=True), + Column('ordinal', Integer, nullable=False), + Column('modelId', Integer, ForeignKey('models.id'), nullable=False), + Column('name', UnicodeText, nullable=False), + Column('description', UnicodeText, nullable=False, default=u""), + Column('active', Boolean, nullable=False, default=True), + # formats: question/answer/last(not used) + Column('qformat', UnicodeText, nullable=False), + Column('aformat', UnicodeText, nullable=False), + Column('lformat', UnicodeText), + # question/answer editor format (not used yet) + Column('qedformat', UnicodeText), + Column('aedformat', UnicodeText), + Column('questionInAnswer', Boolean, nullable=False, default=False), + # display + Column('questionFontFamily', UnicodeText, default=u"Arial"), + Column('questionFontSize', Integer, default=20), + Column('questionFontColour', String(7), default=u"#000000"), + Column('questionAlign', Integer, default=0), + Column('answerFontFamily', UnicodeText, default=u"Arial"), + Column('answerFontSize', Integer, default=20), + Column('answerFontColour', String(7), default=u"#000000"), + Column('answerAlign', Integer, default=0), + Column('lastFontFamily', UnicodeText, default=u"Arial"), + Column('lastFontSize', Integer, default=20), + Column('lastFontColour', String(7), default=u"#000000"), + Column('editQuestionFontFamily', UnicodeText, default=None), + Column('editQuestionFontSize', Integer, default=None), + Column('editAnswerFontFamily', UnicodeText, default=None), + Column('editAnswerFontSize', Integer, default=None)) + +class CardModel(object): + """Represents how to generate the front and back of a card.""" + def __init__(self, name=u"", description=u"", + qformat=u"q", aformat=u"a", active=True): + self.name = name + self.description = description + self.qformat = qformat + self.aformat = aformat + self.active = active + self.id = genID() + + def renderQA(self, card, fact, type, format="text"): + "Render fact into card based on card model." + if type == "question": field = self.qformat + elif type == "answer": field = self.aformat + htmlFields = {} + htmlFields.update(fact) + alltags = parseTags(card.tags + "," + + card.fact.tags + "," + + card.cardModel.name + "," + + card.fact.model.tags) + htmlFields['tags'] = ", ".join(alltags) + textFields = {} + textFields.update(htmlFields) + # add per-field formatting + for (k, v) in htmlFields.items(): + # generate pure text entries + htmlFields["text:"+k] = v + textFields["text:"+k] = v + if v: + # convert newlines to html & add spans to fields + v = v.replace("\n", "
    ") + htmlFields[k] = '%s' % (k.replace(" ",""), v) + try: + html = field % htmlFields + text = field % textFields + except (KeyError, TypeError, ValueError): + return _("[invalid format; see model properties]") + if not html: + html = _("[empty]") + text = _("[empty]") + if format == "text": + return text + # add outer div & alignment (with tables due to qt's html handling) + html = '

    %s
    ' % (type, html) + attr = type + 'Align' + if getattr(self, attr) == 0: + align = "center" + elif getattr(self, attr) == 1: + align = "left" + else: + align = "right" + html = (("
    " % align) + + html + "
    ") + return html + + def renderQASQL(self, type, factId): + "Render QA in pure SQL, with no HTML generation." + fields = dict(object_session(self).all(""" +select fieldModels.name, fields.value from fields, fieldModels +where +fields.factId = :factId and +fields.fieldModelId = fieldModels.id""", factId=factId)) + fields['tags'] = u"" + for (k, v) in fields.items(): + fields["text:"+k] = v + if type == "q": format = self.qformat + else: format = self.aformat + try: + return format % fields + except (KeyError, TypeError, ValueError): + return _("[empty]") + + def css(self): + "Return the CSS markup for this card." + t = "" + for type in ("question", "answer"): + t += ".%s { font-family: \"%s\"; color: %s; font-size: %dpx; }\n" % ( + type, + toPlatformFont(getattr(self, type+"FontFamily")), + getattr(self, type+"FontColour"), + getattr(self, type+"FontSize")) + return t + +mapper(CardModel, cardModelsTable) + +# Model table +########################################################################## + +modelsTable = Table( + 'models', metadata, + Column('id', Integer, primary_key=True), + Column('deckId', Integer, ForeignKey("decks.id", use_alter=True, name="deckIdfk")), + Column('created', Float, nullable=False, default=time.time), + Column('modified', Float, nullable=False, default=time.time), + Column('tags', UnicodeText, nullable=False, default=u""), + Column('name', UnicodeText, nullable=False), + Column('description', UnicodeText, nullable=False, default=u""), + Column('features', UnicodeText, nullable=False, default=u""), + Column('spacing', Float, nullable=False, default=0.1), + Column('initialSpacing', Float, nullable=False, default=600)) + +class Model(object): + "Defines the way a fact behaves, what fields it can contain, etc." + def __init__(self, name=u"", description=u""): + self.name = name + self.description = description + self.id = genID() + + def setModified(self): + self.modified = time.time() + + def addFieldModel(self, field): + "Add a field model." + self.fieldModels.append(field) + s = object_session(self) + if s: + s.flush() + + def addCardModel(self, card): + "Add a card model." + self.cardModels.append(card) + s = object_session(self) + if s: + s.flush() + +mapper(Model, modelsTable, properties={ + 'fieldModels': relation(FieldModel, backref='model', + collection_class=ordering_list('ordinal'), + order_by=[fieldModelsTable.c.ordinal], + cascade="all, delete-orphan"), + 'cardModels': relation(CardModel, backref='model', + collection_class=ordering_list('ordinal'), + order_by=[cardModelsTable.c.ordinal], + cascade="all, delete-orphan"), + }) + +# Model deletions +########################################################################## + +modelsDeletedTable = Table( + 'modelsDeleted', metadata, + Column('modelId', Integer, ForeignKey("models.id"), + nullable=False), + Column('deletedTime', Float, nullable=False)) diff --git a/anki/sound.py b/anki/sound.py new file mode 100644 index 000000000..7ccf26274 --- /dev/null +++ b/anki/sound.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Sound support +============================== +""" +__docformat__ = 'restructuredtext' + +import re, sys + +try: + import pygame + pygame.mixer.pre_init(44100,-16,2, 1024 * 3) + pygame.mixer.init() + soundsAvailable = True +except: + soundsAvailable = False + print "Warning, pygame not available. No sounds will play." + +def playPyGame(path): + "Play a sound. Expects a unicode pathname." + if not soundsAvailable: + return + path = path.encode(sys.getfilesystemencoding()) + try: + if pygame.mixer.music.get_busy(): + pygame.mixer.music.queue(path) + else: + pygame.mixer.music.load(path) + pygame.mixer.music.play() + except pygame.error: + return + +def playFromText(text): + for match in re.findall("\[sound:(.*?)\]", text): + play(match) + +def stripSounds(text): + return re.sub("\[sound:.*?\]", "", text) + +def hasSound(text): + return re.search("\[sound:.*?\]", text) is not None + +# Mac audio support +########################################################################## + +try: + from AppKit import NSSound, NSObject + + queue = [] + current = None + + class Sound(NSObject): + + def init(self): + return self + + def sound_didFinishPlaying_(self, sound, bool): + global current + while 1: + if not queue: + break + next = queue.pop(0) + if play_(next): + break + + s = Sound.new() + + def playOSX(path): + global current + if current: + if current.isPlaying(): + queue.append(path) + return + # new handle + play_(path) + + def play_(path): + global current + current = NSSound.alloc() + current = current.initWithContentsOfFile_byReference_(path, True) + if not current: + return False + current.setDelegate_(s) + current.play() + return True +except ImportError: + pass + +if sys.platform.startswith("darwin"): + play = playOSX +else: + play = playPyGame diff --git a/anki/stats.py b/anki/stats.py new file mode 100644 index 000000000..18e9660a7 --- /dev/null +++ b/anki/stats.py @@ -0,0 +1,581 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Statistical tracking and reports +================================= +""" +__docformat__ = 'restructuredtext' + +# we track statistics over the life of the deck, and per-day +STATS_LIFE = 0 +STATS_DAY = 1 + +import unicodedata, time, sys, os, datetime +import anki, anki.utils +from datetime import date +from anki.db import * +from anki.lang import _ + +# Tracking stats on the DB +########################################################################## + +statsTable = Table( + 'stats', metadata, + Column('id', Integer, primary_key=True), + Column('type', Integer, nullable=False), + Column('day', Date, nullable=False, default=date.today), + Column('reps', Integer, nullable=False, default=0), + Column('averageTime', Float, nullable=False, default=0), + Column('reviewTime', Float, nullable=False, default=0), + # next two columns no longer used + Column('distractedTime', Float, nullable=False, default=0), + Column('distractedReps', Integer, nullable=False, default=0), + Column('newEase0', Integer, nullable=False, default=0), + Column('newEase1', Integer, nullable=False, default=0), + Column('newEase2', Integer, nullable=False, default=0), + Column('newEase3', Integer, nullable=False, default=0), + Column('newEase4', Integer, nullable=False, default=0), + Column('youngEase0', Integer, nullable=False, default=0), + Column('youngEase1', Integer, nullable=False, default=0), + Column('youngEase2', Integer, nullable=False, default=0), + Column('youngEase3', Integer, nullable=False, default=0), + Column('youngEase4', Integer, nullable=False, default=0), + Column('matureEase0', Integer, nullable=False, default=0), + Column('matureEase1', Integer, nullable=False, default=0), + Column('matureEase2', Integer, nullable=False, default=0), + Column('matureEase3', Integer, nullable=False, default=0), + Column('matureEase4', Integer, nullable=False, default=0)) + +class Stats(object): + def __init__(self): + self.day = date.today() + self.reps = 0 + self.averageTime = 0 + self.reviewTime = 0 + self.distractedTime = 0 + self.distractedReps = 0 + self.newEase0 = 0 + self.newEase1 = 0 + self.newEase2 = 0 + self.newEase3 = 0 + self.newEase4 = 0 + self.youngEase0 = 0 + self.youngEase1 = 0 + self.youngEase2 = 0 + self.youngEase3 = 0 + self.youngEase4 = 0 + self.matureEase0 = 0 + self.matureEase1 = 0 + self.matureEase2 = 0 + self.matureEase3 = 0 + self.matureEase4 = 0 + + def fromDB(self, s, id): + r = s.first("select * from stats where id = :id", id=id) + (self.id, + self.type, + self.day, + self.reps, + self.averageTime, + self.reviewTime, + self.distractedTime, + self.distractedReps, + self.newEase0, + self.newEase1, + self.newEase2, + self.newEase3, + self.newEase4, + self.youngEase0, + self.youngEase1, + self.youngEase2, + self.youngEase3, + self.youngEase4, + self.matureEase0, + self.matureEase1, + self.matureEase2, + self.matureEase3, + self.matureEase4) = r + self.day = datetime.date(*[int(i) for i in self.day.split("-")]) + + def create(self, s, type, day): + self.type = type + self.day = day + s.execute("""insert into stats +(type, day, reps, averageTime, reviewTime, distractedTime, distractedReps, +newEase0, newEase1, newEase2, newEase3, newEase4, youngEase0, youngEase1, +youngEase2, youngEase3, youngEase4, matureEase0, matureEase1, matureEase2, +matureEase3, matureEase4) values (:type, :day, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)""", self.__dict__) + self.id = s.scalar( + "select id from stats where type = :type and day = :day", + type=type, day=day) + + def toDB(self, s): + assert self.id + s.execute("""update stats set +type=:type, +day=:day, +reps=:reps, +averageTime=:averageTime, +reviewTime=:reviewTime, +newEase0=:newEase0, +newEase1=:newEase1, +newEase2=:newEase2, +newEase3=:newEase3, +newEase4=:newEase4, +youngEase0=:youngEase0, +youngEase1=:youngEase1, +youngEase2=:youngEase2, +youngEase3=:youngEase3, +youngEase4=:youngEase4, +matureEase0=:matureEase0, +matureEase1=:matureEase1, +matureEase2=:matureEase2, +matureEase3=:matureEase3, +matureEase4=:matureEase4 +where id = :id""", self.__dict__) + +mapper(Stats, statsTable) + +def updateAllStats(s, gs, ds, card, ease, oldState): + "Update global and daily statistics." + updateStats(s, gs, card, ease, oldState) + updateStats(s, ds, card, ease, oldState) + +def updateStats(s, stats, card, ease, oldState): + stats.reps += 1 + delay = card.thinkingTime() + if delay >= 60: + # make a guess as to the time spent answering + stats.reviewTime += stats.averageTime + else: + stats.reviewTime += delay + stats.averageTime = ( + stats.reviewTime / float(stats.reps)) + # update eases + attr = oldState + "Ease%d" % ease + setattr(stats, attr, getattr(stats, attr) + 1) + stats.toDB(s) + +def globalStats(s): + type = STATS_LIFE + today = date.today() + id = s.scalar("select id from stats where type = :type", + type=type) + stats = Stats() + if id: + stats.fromDB(s, id) + return stats + else: + stats.create(s, type, today) + stats.type = type + return stats + +def dailyStats(s): + type = STATS_DAY + today = date.today() + id = s.scalar("select id from stats where type = :type and day = :day", + type=type, day=today) + stats = Stats() + if id: + stats.fromDB(s, id) + return stats + else: + stats.create(s, type, today) + return stats + +def summarizeStats(stats, pre=""): + "Generate percentages and total counts for STATS. Optionally prefix." + cardTypes = ("new", "young", "mature") + h = {} + # total counts + ############### + for type in cardTypes: + # total yes/no for type, eg. gNewYes + h[pre + type.capitalize() + "No"] = (getattr(stats, type + "Ease0") + + getattr(stats, type + "Ease1")) + h[pre + type.capitalize() + "Yes"] = (getattr(stats, type + "Ease2") + + getattr(stats, type + "Ease3") + + getattr(stats, type + "Ease4")) + # total for type, eg. gNewTotal + h[pre + type.capitalize() + "Total"] = ( + h[pre + type.capitalize() + "No"] + + h[pre + type.capitalize() + "Yes"]) + # total yes/no, eg. gYesTotal + for answer in ("yes", "no"): + num = 0 + for type in cardTypes: + num += h[pre + type.capitalize() + answer.capitalize()] + h[pre + answer.capitalize() + "Total"] = num + # total over all, eg. gTotal + num = 0 + for type in cardTypes: + num += h[pre + type.capitalize() + "Total"] + h[pre + "Total"] = num + # percentages + ############## + for type in cardTypes: + # total yes/no % by type, eg. gNewYes% + for answer in ("yes", "no"): + setPercentage(h, pre + type.capitalize() + answer.capitalize(), + pre + type.capitalize()) + for answer in ("yes", "no"): + # total yes/no, eg. gYesTotal% + setPercentage(h, pre + answer.capitalize() + "Total", pre) + h[pre + 'AverageTime'] = stats.averageTime + h[pre + 'ReviewTime'] = stats.reviewTime + return h + +def setPercentage(h, a, b): + try: + h[a + "%"] = (h[a] / float(h[b + "Total"])) * 100 + except ZeroDivisionError: + h[a + "%"] = 0 + +def getStats(s, gs, ds): + "Return a handy dictionary exposing a number of internal stats." + h = {} + h.update(summarizeStats(gs, "g")) + h.update(summarizeStats(ds, "d")) + return h + +# Card stats +########################################################################## + +class CardStats(object): + + def __init__(self, deck, card): + self.deck = deck + self.card = card + + def report(self): + c = self.card + self.txt = "" + self.addLine(_("Added"), self.strTime(c.created)) + if c.firstAnswered: + self.addLine(_("First review"), self.strTime(c.firstAnswered)) + self.addLine(_("Changed"), self.strTime(c.modified)) + next = time.time() - c.due + if next > 0: + next = _("%s ago") % anki.utils.fmtTimeSpan(next) + else: + next = _("in %s") % anki.utils.fmtTimeSpan(abs(next)) + self.addLine(_("Next due"), next) + self.addLine(_("Current interval"), "%0.2f days" % c.interval) + if c.interval != c.lastInterval: + # don't show the last interval if it hasn't been updated yet + self.addLine(_("Last interval"), "%0.2f days" % c.lastInterval) + self.addLine(_("Current factor"), "%0.2f" % c.factor) + self.addLine(_("Last factor"), "%0.2f" % c.lastFactor) + self.addLine(_("Review count"), c.reps) + if c.reps: + self.addLine(_("Correct count"), "%d (%0.2f%%)" % ( + c.yesCount, (c.yesCount / float(c.reps))*100)) + self.addLine(_("Repeatedly correct"), c.successive) + self.addLine(_("Average time"), _("%0.1f seconds") % + c.averageTime) + self.addLine(_("Total time"), _("%0.1f seconds") % + c.reviewTime) + if self.deck.cardIsNew(c): + state = _("First time") + elif self.deck.cardIsBeingLearnt(c): + state = _("Young") + else: + state = _("Mature") + self.addLine(_("State"), state) + if c.tags: + self.addLine(_("Card tags"), c.tags) + self.addLine(_("Card model tags"), c.cardModel.name) + if c.fact.model.tags: + self.addLine(_("Model tags"), c.fact.model.tags) + if c.fact.tags: + self.addLine(_("Fact tags"), c.fact.tags) + self.txt += "
    " + return self.txt + + def addLine(self, k, v): + self.txt += "%s%s" % (k, v) + + def strTime(self, tm): + s = anki.utils.fmtTimeSpan(time.time() - tm) + return _("%s ago") % s + +# Deck stats (specific to the 'sched' scheduler) +########################################################################## + +class DeckStats(object): + + def __init__(self, deck): + self.deck = deck + + def report(self): + "Return an HTML string with a report." + if self.deck.isEmpty(): + return _("Please add some cards first.

    ") + d = self.deck + html="

    " + _("Deck Statistics") + "

    " + html += _("Deck created: %s ago
    ") % self.createdTimeStr() + total = d.cardCount() + new = d.newCardCount() + young = d.youngCardCount() + old = d.matureCardCount() + newP = new / float(total) * 100 + youngP = young / float(total) * 100 + oldP = old / float(total) * 100 + stats = d.getStats() + (stats["new"], stats["newP"]) = (new, newP) + (stats["old"], stats["oldP"]) = (old, oldP) + (stats["young"], stats["youngP"]) = (young, youngP) + html += _("Total number of cards: %d

    ") % total + + html += _("Card counts
    ") + html += _("Mature cards: %(old)d " + "(%(oldP)0.2f%%)
    ") % stats + html += _("Young cards: %(young)d " + "(%(youngP)0.2f%%)
    ") % stats + html += _("Unseen cards: %(new)d " + "(%(newP)0.2f%%)

    ") % stats + + html += _("Correct answers
    ") + html += _("Mature cards: %(gMatureYes%)0.1f%% " + "(%(gMatureYes)d of %(gMatureTotal)d)
    ") % stats + html += _("Young cards: %(gYoungYes%)0.1f%% " + "(%(gYoungYes)d of %(gYoungTotal)d)
    ") % stats + html += _("First-seen cards: %(gNewYes%)0.1f%% " + "(%(gNewYes)d of %(gNewTotal)d)

    ") % stats + # average pending time + existing = d.cardCount() - d.newCardCount() + avgInt = self.getAverageInterval() + def tr(a, b): + return "%s%s" % (a, b) + if existing and avgInt: + html += _("Averages
    ") + html += "" + html += tr(_("Interval"), _("%0.0f days") % avgInt) + html += tr(_("Average reps"), _("%0.1f cards/day") % ( + self.getSumInverseRoundInterval())) + html += tr(_("Reps next week"), _("%0.1f cards/day") % ( + self.getWorkloadPeriod(7))) + html += tr(_("Reps next month"), _("%0.1f cards/day") % ( + self.getWorkloadPeriod(30))) + html += tr(_("Reps last week"), _("%0.1f cards/day") % ( + self.getPastWorkloadPeriod(7))) + html += tr(_("Reps last month"), _("%0.1f cards/day") % ( + self.getPastWorkloadPeriod(30))) + html += tr(_("Avg. added"), _("%(a)d/day, %(b)d/mon") % { + 'a': self.newAverage(), 'b': self.newAverage()*30}) + np = self.getNewPeriod(7) + html += tr(_("Added last week"), _("%(a)d (%(b)0.1f/day)") % ( + {'a': np, 'b': np / float(7)})) + np = self.getNewPeriod(30) + html += tr(_("Added last month"), _("%(a)d (%(b)0.1f/day)") % ( + {'a': np, 'b': np / float(30)})) + np = self.getFirstPeriod(7) + html += tr(_("First last week"), _("%(a)d (%(b)0.1f/day)") % ( + {'a': np, 'b': np / float(7)})) + np = self.getFirstPeriod(30) + html += tr(_("First last month"), _("%(a)d (%(b)0.1f/day)") % ( + {'a': np, 'b': np / float(30)})) + html += "
    " + return html + + def getAverageInterval(self): + return self.deck.s.scalar( + "select sum(interval) / count(interval) from cards " + "where cards.reps > 0") or 0 + + def intervalReport(self, intervals, labels, total): + boxes = self.splitIntoIntervals(intervals) + keys = boxes.keys() + keys.sort() + html = "" + for key in keys: + html += ("%s" + + "%d%0.2f%%") % ( + labels[key], + boxes[key], + boxes[key] / float(total) * 100) + return html + + def splitIntoIntervals(self, intervals): + boxes = {} + n = 0 + for i in range(len(intervals) - 1): + (min, max) = (intervals[i], intervals[i+1]) + for c in self.deck: + if c.interval > min and c.interval <= max: + boxes[n] = boxes.get(n, 0) + 1 + n += 1 + return boxes + + def newAverage(self): + "Average number of new cards added each day." + return self.deck.cardCount() / max(1, self.ageInDays()) + + def createdTimeStr(self): + return anki.utils.fmtTimeSpan(time.time() - self.deck.created) + + def ageInDays(self): + return (time.time() - self.deck.created) / 86400.0 + + def getSumInverseRoundInterval(self): + return self.deck.s.scalar( + "select sum(1/round(max(interval, 1)+0.5)) from cards " + "where cards.reps > 0 " + "and priority > 0") or 0 + + def getWorkloadPeriod(self, period): + cutoff = time.time() + 86400 * period + return (self.deck.s.scalar(""" +select count(id) from cards +where combinedDue < :cutoff +and priority > 0""", cutoff=cutoff) or 0) / float(period) + + def getPastWorkloadPeriod(self, period): + cutoff = time.time() - 86400 * period + return (self.deck.s.scalar(""" +select count(id) from reviewHistory +where time > :cutoff""", cutoff=cutoff) or 0) / float(period) + + def getNewPeriod(self, period): + cutoff = time.time() - 86400 * period + return (self.deck.s.scalar(""" +select count(id) from cards +where created > :cutoff""", cutoff=cutoff) or 0) + + def getFirstPeriod(self, period): + cutoff = time.time() - 86400 * period + return (self.deck.s.scalar(""" +select count(id) from reviewHistory +where reps = 1 and time > :cutoff""", cutoff=cutoff) or 0) + +# Kanji stats +########################################################################## + +def asHTMLDocument(text): + "Return text wrapped in a HTML document." + return (""" + + + + + + + %s + + + """ % text.encode("utf-8")) + +def isKanji(unichar): + try: + return unicodedata.name(unichar).find('CJK UNIFIED IDEOGRAPH') >= 0 + except ValueError: + # a control character + return False + +class KanjiStats(object): + + def __init__(self, deck): + self.deck = deck + self._gradeHash = dict() + for (name, chars), grade in zip(self.kanjiGrades, + xrange(len(self.kanjiGrades))): + for c in chars: + self._gradeHash[c] = grade + + def kanjiGrade(self, unichar): + return self._gradeHash.get(unichar, 0) + + # FIXME: as it's html, the width doesn't matter + def kanjiCountStr(self, gradename, count, total=0, width=0): + d = {'count': self.rjustfig(count, width), 'gradename': gradename} + if total: + d['total'] = self.rjustfig(total, width) + d['percent'] = float(count)/total*100 + return _("%(gradename)s: %(count)s of %(total)s (%(percent)0.1f%%).") % d + else: + return _("%(count)s %(gradename)s kanji.") % d + + def rjustfig(self, n, width): + n = unicode(n) + return n + " " * (width - len(n)) + + def genKanjiSets(self): + self.kanjiSets = [set([]) for g in self.kanjiGrades] + all = "".join(self.deck.s.column0(""" +select value from cards, fields +where +cards.reps > 0 and +cards.factId = fields.factId +""")) + for u in all: + if isKanji(u): + self.kanjiSets[self.kanjiGrade(u)].add(u) + + def report(self): + self.genKanjiSets() + counts = [(name, len(found), len(all)) \ + for (name, all), found in zip(self.kanjiGrades, self.kanjiSets)] + out = (_("

    Kanji statistics

    The %d seen cards in this deck " + "contain:") % self.deck.seenCardCount() + + "
      " + + # total kanji + _("
    • %d total unique kanji.
    • ") % + sum([c[1] for c in counts]) + + # total joyo + "
    • %s
    • " % self.kanjiCountStr( + u'Jouyou',sum([c[1] for c in counts[1:8]]), + sum([c[2] for c in counts[1:8]])) + + # total jinmei + "
    • %s
    • " % self.kanjiCountStr(*counts[8]) + + # total non-joyo + "
    • %s
    • " % self.kanjiCountStr(*counts[0])) + + out += "

    " + _(u"Jouyou levels:") + "

      " + L = ["
    • " + self.kanjiCountStr(c[0],c[1],c[2], width=3) + "
    • " + for c in counts[1:8]] + out += "".join(L) + out += "
    " + return out + + def missingReport(self): + out = "

    Missing kanji

    " + for grade in range(1, 9): + missing = "".join(self.missingInGrade(grade)) + if not missing: + continue + out += "

    " + self.kanjiGrades[grade][0] + "

    " + out += "" + while 1: + if not missing: + break + # edict will take up to about 10 kanji at once + out += self.edictKanjiLink(missing[0:10]) + missing = missing[10:] + out += "" + return out + "
    " + + def edictKanjiLink(self, kanji): + base="http://www.csse.monash.edu.au/~jwb/cgi-bin/wwwjdic.cgi?1MMJ" + url=base + kanji + return '%s' % (url, kanji) + + def missingInGrade(self, gradeNum): + existingKanji = self.kanjiSets[gradeNum] + totalKanji = self.kanjiGrades[gradeNum][1] + return [k for k in totalKanji if k not in existingKanji] + + kanjiGrades = [ + (u'non-jouyou', ''), + (u'Grade 1', u'一右雨円王音下火花貝学気休玉金九空月犬見五口校左三山四子糸字耳七車手十出女小上森人水正生青石赤先千川早草足村大男竹中虫町天田土二日入年白八百文本名木目夕立力林六'), + (u'Grade 2', u'引羽雲園遠黄何夏家科歌画会回海絵外角楽活間丸岩顔帰汽記弓牛魚京強教近兄形計元原言古戸午後語交光公工広考行高合国黒今才細作算姉市思止紙寺時自室社弱首秋週春書少場色食心新親図数星晴声西切雪線船前組走多太体台谷知地池茶昼朝長鳥直通弟店点電冬刀東当答頭同道読内南肉馬買売麦半番父風分聞米歩母方北妹毎万明鳴毛門夜野矢友曜用来理里話'), + (u'Grade 3', u'悪安暗委意医育員飲院運泳駅央横屋温化荷界開階寒感漢館岸期起客宮急球究級去橋業局曲銀区苦具君係軽決血研県庫湖向幸港号根祭坂皿仕使始指死詩歯事持次式実写者主取守酒受州拾終習集住重宿所暑助勝商昭消章乗植深申真神身進世整昔全想相送息速族他打対待代第題炭短談着柱注丁帳調追定庭笛鉄転登都度島投湯等豆動童農波配倍箱畑発反板悲皮美鼻筆氷表病秒品負部服福物平返勉放味命面問役薬油有由遊予様洋羊葉陽落流旅両緑礼列練路和'), + (u'Grade 4', u'愛案以位囲胃衣印栄英塩億加果課貨芽改械害街各覚完官管観関願喜器希旗機季紀議救求泣給挙漁競共協鏡極訓軍郡型径景芸欠結健建験固候功好康航告差最菜材昨刷察札殺参散産残司史士氏試児治辞失借種周祝順初唱松焼照省笑象賞信臣成清静席積折節説戦浅選然倉巣争側束続卒孫帯隊達単置仲貯兆腸低停底的典伝徒努灯働堂得特毒熱念敗梅博飯費飛必標票不付夫府副粉兵別変辺便包法望牧末満未脈民無約勇要養浴利陸料良量輪類令例冷歴連労老録'), + (u'Grade 5', u'圧易移因営永衛液益演往応恩仮価可河過賀解快格確額刊幹慣眼基寄規技義逆久旧居許境興均禁句群経潔件券検険減現限個故護効厚構耕講鉱混査再妻採災際在罪財桜雑賛酸師志支枝資飼似示識質舎謝授修術述準序承招証常情条状織職制勢性政精製税績責接設絶舌銭祖素総像増造則測属損態貸退団断築張提程敵適統導銅徳独任燃能破判版犯比肥非備俵評貧婦富布武復複仏編弁保墓報豊暴貿防務夢迷綿輸余預容率略留領'), + (u'Grade 6', u'異遺域宇映延沿我灰拡閣革割株巻干看簡危揮机貴疑吸供胸郷勤筋敬系警劇激穴憲権絹厳源呼己誤后孝皇紅鋼降刻穀骨困砂座済裁策冊蚕姿私至視詞誌磁射捨尺若樹収宗就衆従縦縮熟純処署諸除傷将障城蒸針仁垂推寸盛聖誠宣専泉洗染善創奏層操窓装臓蔵存尊宅担探誕暖段値宙忠著庁潮頂賃痛展党糖討届難乳認納脳派俳拝背肺班晩否批秘腹奮並閉陛片補暮宝訪亡忘棒枚幕密盟模訳優郵幼欲翌乱卵覧裏律臨朗論'), + (u'JuniorHS', u'亜哀握扱依偉威尉慰為維緯違井壱逸稲芋姻陰隠韻渦浦影詠鋭疫悦謁越閲宴援炎煙猿縁鉛汚凹奥押欧殴翁沖憶乙卸穏佳嫁寡暇架禍稼箇華菓蚊雅餓介塊壊怪悔懐戒拐皆劾慨概涯該垣嚇核殻獲穫較郭隔岳掛潟喝括渇滑褐轄且刈乾冠勘勧喚堪寛患憾換敢棺款歓汗環甘監緩缶肝艦貫還鑑閑陥含頑企奇岐幾忌既棋棄祈軌輝飢騎鬼偽儀宜戯擬欺犠菊吉喫詰却脚虐丘及朽窮糾巨拒拠虚距享凶叫峡恐恭挟況狂狭矯脅響驚仰凝暁斤琴緊菌襟謹吟駆愚虞偶遇隅屈掘靴繰桑勲薫傾刑啓契恵慶憩掲携渓継茎蛍鶏迎鯨撃傑倹兼剣圏堅嫌懸献肩謙賢軒遣顕幻弦玄孤弧枯誇雇顧鼓互呉娯御悟碁侯坑孔巧恒慌抗拘控攻更江洪溝甲硬稿絞綱肯荒衡貢購郊酵項香剛拷豪克酷獄腰込墾婚恨懇昆紺魂佐唆詐鎖債催宰彩栽歳砕斎載剤咲崎削搾索錯撮擦傘惨桟暫伺刺嗣施旨祉紫肢脂諮賜雌侍慈滋璽軸執湿漆疾芝赦斜煮遮蛇邪勺爵酌釈寂朱殊狩珠趣儒寿需囚愁秀臭舟襲酬醜充柔汁渋獣銃叔淑粛塾俊瞬准循旬殉潤盾巡遵庶緒叙徐償匠升召奨宵尚床彰抄掌昇晶沼渉焦症硝礁祥称粧紹肖衝訟詔詳鐘丈冗剰壌嬢浄畳譲醸錠嘱飾殖触辱伸侵唇娠寝審慎振浸紳薪診辛震刃尋甚尽迅陣酢吹帥炊睡粋衰遂酔錘随髄崇枢据杉澄瀬畝是姓征牲誓請逝斉隻惜斥析籍跡拙摂窃仙占扇栓潜旋繊薦践遷銑鮮漸禅繕塑措疎礎租粗訴阻僧双喪壮捜掃挿曹槽燥荘葬藻遭霜騒憎贈促即俗賊堕妥惰駄耐怠替泰滞胎袋逮滝卓択拓沢濯託濁諾但奪脱棚丹嘆淡端胆鍛壇弾恥痴稚致遅畜蓄逐秩窒嫡抽衷鋳駐弔彫徴懲挑眺聴脹超跳勅朕沈珍鎮陳津墜塚漬坪釣亭偵貞呈堤帝廷抵締艇訂逓邸泥摘滴哲徹撤迭添殿吐塗斗渡途奴怒倒凍唐塔悼搭桃棟盗痘筒到謄踏逃透陶騰闘洞胴峠匿督篤凸突屯豚曇鈍縄軟尼弐如尿妊忍寧猫粘悩濃把覇婆廃排杯輩培媒賠陪伯拍泊舶薄迫漠爆縛肌鉢髪伐罰抜閥伴帆搬畔繁般藩販範煩頒盤蛮卑妃彼扉披泌疲碑罷被避尾微匹姫漂描苗浜賓頻敏瓶怖扶敷普浮符腐膚譜賦赴附侮舞封伏幅覆払沸噴墳憤紛雰丙併塀幣弊柄壁癖偏遍舗捕穂募慕簿倣俸奉峰崩抱泡砲縫胞芳褒邦飽乏傍剖坊妨帽忙房某冒紡肪膨謀僕墨撲朴没堀奔翻凡盆摩磨魔麻埋膜又抹繭慢漫魅岬妙眠矛霧婿娘銘滅免茂妄猛盲網耗黙戻紋匁厄躍柳愉癒諭唯幽悠憂猶裕誘雄融与誉庸揚揺擁溶窯謡踊抑翼羅裸頼雷絡酪欄濫吏履痢離硫粒隆竜慮虜了僚寮涼猟療糧陵倫厘隣塁涙累励鈴隷零霊麗齢暦劣烈裂廉恋錬炉露廊楼浪漏郎賄惑枠湾腕'), + (u'Jinmeiyou', u'阿葵茜渥旭梓絢綾鮎杏伊惟亥郁磯允胤卯丑唄叡瑛艶苑於旺伽嘉茄霞魁凱馨叶樺鎌茅侃莞巌伎嬉毅稀亀誼鞠橘亨匡喬尭桐錦欣欽芹衿玖矩駒熊栗袈圭慧桂拳絃胡虎伍吾梧瑚鯉倖宏弘昂晃浩紘鴻嵯沙瑳裟哉采冴朔笹皐燦獅爾蒔汐鹿偲紗洲峻竣舜駿淳醇曙渚恕庄捷昌梢菖蕉丞穣晋榛秦須翠瑞嵩雛碩曽爽惣綜聡蒼汰黛鯛鷹啄琢只辰巽旦檀智猪暢蝶椎槻蔦椿紬鶴悌汀禎杜藤憧瞳寅酉惇敦奈那凪捺楠虹乃之巴萩肇鳩隼斐緋眉柊彦媛彪彬芙楓蕗碧甫輔朋萌鳳鵬睦槙柾亦麿巳稔椋孟也冶耶弥靖佑宥柚湧祐邑楊耀蓉遥嵐藍蘭李梨璃琉亮凌瞭稜諒遼琳麟瑠伶嶺怜玲蓮呂禄倭亘侑勁奎崚彗昴晏晨晟暉栞椰毬洸洵滉漱澪燎燿瑶皓眸笙綺綸翔脩茉莉菫詢諄赳迪頌颯黎凜熙') + ] diff --git a/anki/stdmodels.py b/anki/stdmodels.py new file mode 100644 index 000000000..c6d1adb36 --- /dev/null +++ b/anki/stdmodels.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Standard models +============================================================== +""" + +from anki.models import Model, CardModel, FieldModel +from anki.lang import _ + +models = {} + +def byName(name): + fn = models.get(name) + if fn: + return fn() + raise ValueError("No such model available!") + +def names(): + return models.keys() + +# these are provided for convenience. all of the fields can be changed in real +# time and they will be stored with the deck. + +# Basic +########################################################################## + +def BasicModel(): + m = Model(_('Basic'), + _('A basic flashcard with a front and a back.\n' + 'Questions are asked from front to back by default.\n\n' + 'Please consider customizing this model, rather than\n' + 'using it verbatim: field names like "expression" are\n' + 'clearer than "front" and "back", and will ensure\n' + 'that your entries are consistent.')) + m.addFieldModel(FieldModel(u'Front', _('A question.'), True, True)) + m.addFieldModel(FieldModel(u'Back', _('The answer.'), True, True)) + m.addCardModel(CardModel(u'Front to back', _('Front to back'), + u'%(Front)s', u'%(Back)s')) + m.addCardModel(CardModel(u'Back to front', _('Back to front'), + u'%(Back)s', u'%(Front)s', active=False)) + return m +models['Basic'] = BasicModel + +# Japanese +########################################################################## + +def JapaneseModel(): + m = Model(_("Japanese"), + _(""" +The reading field is automatically generated by default, +and shows the reading for the expression. For words that +are normally written in hiragana or katakana and don't +need a reading, you can put the word in the expression +field, and leave the reading field blank. A reading will +will not automatically be generated for words written +in only hiragana or katakana. + +Note that the automatic generation of meaning is not +perfect, and should be checked before adding cards.""".strip())) + # expression + f = FieldModel(u'Expression', + _('A word or expression written in Kanji.'), True, True) + font = u"Mincho" + f.quizFontSize = 72 + f.quizFontFamily = font + f.editFontFamily = font + f.features = u"Reading source" + m.addFieldModel(f) + # meaning + m.addFieldModel(FieldModel( + u'Meaning', + _('A description in your native language, or Japanese'), True, True)) + # reading + f = FieldModel(u'Reading', + _('Automatically generated by default.'), False, False) + f.quizFontFamily = font + f.editFontFamily = font + f.features = u"Reading destination" + m.addFieldModel(f) + m.addCardModel(CardModel(u"Production", _( + "Actively test your recall by producing the target expression"), + u"%(Meaning)s", + u"%(Expression)s
    %(Reading)s")) + m.addCardModel(CardModel(u"Recognition", _( + "Test your ability to recognize the target expression"), + u"%(Expression)s", + u"%(Reading)s
    %(Meaning)s")) + m.features = u"Japanese" + m.tags = u"Japanese" + return m +models['Japanese'] = JapaneseModel + +# English +########################################################################## + +def EnglishModel(): + m = Model(_("English"), + _(""" +Enter the English expression you want to learn in the 'Expression' field. +Enter a description in Japanese or English in the 'Meaning' field.""".strip())) + m.addFieldModel(FieldModel(u'Expression')) + m.addFieldModel(FieldModel(u'Meaning')) + m.addCardModel(CardModel( + u"Production", _("From the meaning to the English expression."), + u"%(Meaning)s", u"%(Expression)s")) + m.addCardModel(CardModel( + u"Recognition", _("From the English expression to the meaning."), + u"%(Expression)s", u"%(Meaning)s", active=False)) + m.tags = u"English" + return m +models['English'] = EnglishModel + +# Heisig +########################################################################## + +def HeisigModel(): + m = Model(_("Heisig"), + _(""" +A format suitable for Heisig's "Remembering the Kanji". +You are tested from the keyword to the kanji. + +Layout of the test is based on the great work at +http://kanji.koohii.com/ + +The link in the question will list user-contributed +stories. A free login is required.""".strip())) + font = u"Mincho" + f = FieldModel(u'Kanji') + f.quizFontSize = 150 + f.quizFontFamily = font + f.editFontFamily = font + m.addFieldModel(f) + m.addFieldModel(FieldModel(u'Keyword')) + m.addFieldModel(FieldModel(u'Story', u"", False, False)) + m.addFieldModel(FieldModel(u'Stroke count', u"", False, False)) + m.addFieldModel(FieldModel(u'Heisig number', required=False)) + m.addFieldModel(FieldModel(u'Lesson number', u"", False, False)) + m.addCardModel(CardModel( + u"Production", _("From the keyword to the Kanji."), + u"%(Keyword)s
    ", + u"%(Kanji)s
    " + u"画数%(Stroke count)s" + u"%(Heisig number)s
    ")) + m.tags = u"Heisig" + return m +models['Heisig'] = HeisigModel + +# Chinese: Mandarin & Cantonese +########################################################################## + +def CantoneseModel(): + m = Model(_("Cantonese"), + u"") + f = FieldModel(u'Expression', + _('A word or expression written in Hanzi.')) + f.quizFontSize = 72 + f.features = u"Reading source" + m.addFieldModel(f) + m.addFieldModel(FieldModel( + u'Meaning', _('A description in your native language, or Cantonese'))) + f = FieldModel(u'Reading', + _('Automatically generated by default.'), False, False) + f.features = u"Reading destination" + m.addFieldModel(f) + m.addCardModel(CardModel(u"Production", _( + "Actively test your recall by producing the target expression"), + u"%(Meaning)s", + u"%(Expression)s
    %(Reading)s")) + m.addCardModel(CardModel(u"Recognition", _( + "Test your ability to recognize the target expression"), + u"%(Expression)s", + u"%(Reading)s
    %(Meaning)s")) + m.features = u"Cantonese" + m.tags = u"Cantonese" + return m +models['Cantonese'] = CantoneseModel + +def MandarinModel(): + m = Model(_("Mandarin"), + u"") + f = FieldModel(u'Expression', + _('A word or expression written in Hanzi.')) + f.quizFontSize = 72 + f.features = u"Reading source" + m.addFieldModel(f) + m.addFieldModel(FieldModel( + u'Meaning', _( + 'A description in your native language, or Mandarin'))) + f = FieldModel(u'Reading', + _('Automatically generated by default.'), + False, False) + f.features = u"Reading destination" + m.addFieldModel(f) + m.addCardModel(CardModel(u"Production", _( + "Actively test your recall by producing the target expression"), + u"%(Meaning)s", + u"%(Expression)s
    %(Reading)s")) + m.addCardModel(CardModel(u"Recognition", _( + "Test your ability to recognize the target expression"), + u"%(Expression)s", + u"%(Reading)s
    %(Meaning)s")) + m.features = u"Mandarin" + m.tags = u"Mandarin" + return m +models['Mandarin'] = MandarinModel + diff --git a/anki/sync.py b/anki/sync.py new file mode 100644 index 000000000..1d22a8090 --- /dev/null +++ b/anki/sync.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Synchronisation +============================== + +Support for keeping two decks synchronized. Both local syncing and syncing +over HTTP are supported. + +Server implements the following calls: + +getDecks(): return a list of deck names & modtimes +summary(lastSync): a list of all objects changed after lastSync +applyPayload(payload): apply any sent changes and return any changed remote + objects +createDeck(name): create a deck on the server + +""" +__docformat__ = 'restructuredtext' + +import zlib, re, urllib, urllib2, socket, simplejson, time +import os, base64 +from datetime import date +import anki, anki.deck, anki.cards +from anki.errors import * +from anki.models import Model, FieldModel, CardModel +from anki.facts import Fact, Field +from anki.cards import Card +from anki.stats import Stats, globalStats +from anki.history import CardHistoryEntry +from anki.stats import dailyStats, globalStats +from anki.media import checksum +from anki.utils import ids2str +from anki.lang import _ + +if simplejson.__version__ < "1.7.3": + raise "SimpleJSON must be 1.7.3 or later." + +# Protocol 3 code +########################################################################## + +class SyncTools(object): + + def __init__(self, deck=None): + self.deck = deck + self.diffs = {} + self.serverExcludedTags = [] + + # Control + ########################################################################## + + def setServer(self, server): + self.server = server + + def sync(self): + "Sync two decks locally. Reimplement this for finer control." + if not self.prepareSync(): + return + sums = self.summaries() + payload = self.genPayload(sums) + res = self.server.applyPayload(payload) + self.applyPayloadReply(res) + + def prepareSync(self): + "Sync setup. True if sync needed." + self.localTime = self.modified() + self.remoteTime = self.server.modified() + if self.localTime == self.remoteTime: + return False + l = self._lastSync(); r = self.server._lastSync() + if l != r: + self.lastSync = min(l, r) - 600 + else: + self.lastSync = l + return True + + def summaries(self): + return (self.summary(self.lastSync), + self.server.summary(self.lastSync)) + + def genPayload(self, summaries): + (lsum, rsum) = summaries + self.preSyncRefresh() + payload = {} + # first, handle models, facts and cards + for key in self.keys(): + diff = self.diffSummary(lsum, rsum, key) + payload["added-" + key] = self.getObjsFromKey(diff[0], key) + payload["deleted-" + key] = diff[1] + payload["missing-" + key] = diff[2] + self.deleteObjsFromKey(diff[3], key) + # handle the remainder + if self.localTime > self.remoteTime: + payload['deck'] = self.bundleDeck() + payload['stats'] = self.bundleStats() + payload['history'] = self.bundleHistory() + self.deck.lastSync = self.deck.modified + return payload + + def applyPayload(self, payload): + reply = {} + self.preSyncRefresh() + # model, facts and cards + for key in self.keys(): + # send back any requested + reply['added-' + key] = self.getObjsFromKey( + payload['missing-' + key], key) + self.updateObjsFromKey(payload['added-' + key], key) + self.deleteObjsFromKey(payload['deleted-' + key], key) + # send back deck-related stuff if it wasn't sent to us + if not 'deck' in payload: + reply['deck'] = self.bundleDeck() + reply['stats'] = self.bundleStats() + reply['history'] = self.bundleHistory() + self.deck.lastSync = self.deck.modified + else: + self.updateDeck(payload['deck']) + self.updateStats(payload['stats']) + self.updateHistory(payload['history']) + self.postSyncRefresh() + # rebuild priorities on server + cardIds = [x[0] for x in reply['added-cards']] + self.rebuildPriorities(cardIds, self.serverExcludedTags) + return reply + + def applyPayloadReply(self, reply): + # model, facts and cards + for key in self.keys(): + self.updateObjsFromKey(reply['added-' + key], key) + # deck + if 'deck' in reply: + self.updateDeck(reply['deck']) + self.updateStats(reply['stats']) + self.updateHistory(reply['history']) + self.postSyncRefresh() + # rebuild priorities on client + cardIds = [x[0] for x in reply['added-cards']] + self.rebuildPriorities(cardIds) + + def rebuildPriorities(self, cardIds, extraExcludes=[]): + where = "and cards.id in %s" % ids2str(cardIds) + self.deck.updateAllPriorities(extraExcludes=extraExcludes, + where=where) + + def postSyncRefresh(self): + "Flush changes to DB, and reload object associations." + self.deck.s.flush() + self.deck.s.refresh(self.deck) + self.deck.currentModel + + def preSyncRefresh(self): + # ensure global stats are available (queue may not be built) + self.deck._globalStats = globalStats(self.deck.s) + + def payloadChanges(self, payload): + h = { + 'lf': len(payload['added-facts']['facts']), + 'rf': len(payload['missing-facts']), + 'lc': len(payload['added-cards']), + 'rc': len(payload['missing-cards']), + 'lm': len(payload['added-models']), + 'rm': len(payload['missing-models']), + } + if self.server.mediaSupported: + h['lM'] = len(payload['added-media']) + h['rM'] = len(payload['missing-media']) + return h + + def payloadChangeReport(self, payload): + p = self.payloadChanges(payload) + if self.server.mediaSupported: + p['media'] = ( + "Media%(lM)d%(rM)d" % p) + else: + p['media'] = ( + "Mediaoffoff" % p) + return _("""\ + + + + + +%(media)s +
    Added/ChangedHereServer
    Cards%(lc)d%(rc)d
    Facts%(lf)d%(rf)d
    Models%(lm)d%(rm)d
    """) % p + + # Summaries + ########################################################################## + + def summary(self, lastSync): + "Generate a full summary of modtimes for two-way syncing." + # ensure we're flushed first + self.deck.s.flush() + return { + # cards + "cards": self.realTuples(self.deck.s.all( + "select id, modified from cards where modified > :mod", + mod=lastSync)), + "delcards": self.realTuples(self.deck.s.all( + "select cardId, deletedTime from cardsDeleted " + "where deletedTime > :mod", mod=lastSync)), + # facts + "facts": self.realTuples(self.deck.s.all( + "select id, modified from facts where modified > :mod", + mod=lastSync)), + "delfacts": self.realTuples(self.deck.s.all( + "select factId, deletedTime from factsDeleted " + "where deletedTime > :mod", mod=lastSync)), + # models + "models": self.realTuples(self.deck.s.all( + "select id, modified from models where modified > :mod", + mod=lastSync)), + "delmodels": self.realTuples(self.deck.s.all( + "select modelId, deletedTime from modelsDeleted " + "where deletedTime > :mod", mod=lastSync)), + # media + "media": self.realTuples(self.deck.s.all( + "select id, created from media where created > :mod", + mod=lastSync)), + "delmedia": self.realTuples(self.deck.s.all( + "select mediaId, deletedTime from mediaDeleted " + "where deletedTime > :mod", mod=lastSync)), + } + + # Diffing + ########################################################################## + + def diffSummary(self, localSummary, remoteSummary, key): + # list of ids on both ends + lexists = localSummary[key] + ldeleted = localSummary["del"+key] + rexists = remoteSummary[key] + rdeleted = remoteSummary["del"+key] + ldeletedIds = dict(ldeleted) + rdeletedIds = dict(rdeleted) + # to store the results + locallyEdited = [] + locallyDeleted = [] + remotelyEdited = [] + remotelyDeleted = [] + # build a hash of all ids, with value (localMod, remoteMod). + # deleted/nonexisting cards are marked with a modtime of None. + ids = {} + for (id, mod) in rexists: + ids[id] = [None, mod] + for (id, mod) in rdeleted: + ids[id] = [None, None] + for (id, mod) in lexists: + if id in ids: + ids[id][0] = mod + else: + ids[id] = [mod, None] + for (id, mod) in ldeleted: + if id in ids: + ids[id][0] = None + else: + ids[id] = [None, None] + # loop through the hash, determining differences + for (id, (localMod, remoteMod)) in ids.items(): + if localMod and remoteMod: + # changed/existing on both sides + if localMod < remoteMod: + remotelyEdited.append(id) + elif localMod > remoteMod: + locallyEdited.append(id) + elif localMod and not remoteMod: + # if it's missing on server or newer here, sync + if (id not in rdeletedIds or + rdeletedIds[id] < localMod): + locallyEdited.append(id) + else: + remotelyDeleted.append(id) + elif remoteMod and not localMod: + # if it's missing locally or newer there, sync + if (id not in ldeletedIds or + ldeletedIds[id] < remoteMod): + remotelyEdited.append(id) + else: + locallyDeleted.append(id) + else: + if id in ldeletedIds and id not in rdeletedIds: + locallyDeleted.append(id) + elif id in rdeletedIds and id not in ldeletedIds: + remotelyDeleted.append(id) + return (locallyEdited, locallyDeleted, + remotelyEdited, remotelyDeleted) + + # Models + ########################################################################## + + def getModels(self, ids): + return [self.bundleModel(id) for id in ids] + + def bundleModel(self, id): + "Return a model representation suitable for transport." + # force load of lazy attributes + mod = self.deck.s.query(Model).get(id) + mod.fieldModels; mod.cardModels + m = self.dictFromObj(mod) + m['fieldModels'] = [self.bundleFieldModel(fm) for fm in m['fieldModels']] + m['cardModels'] = [self.bundleCardModel(fm) for fm in m['cardModels']] + return m + + def bundleFieldModel(self, fm): + d = self.dictFromObj(fm) + if 'model' in d: del d['model'] + return d + + def bundleCardModel(self, cm): + d = self.dictFromObj(cm) + if 'model' in d: del d['model'] + return d + + def updateModels(self, models): + for model in models: + local = self.getModel(model['id']) + # avoid overwriting any existing card/field models + fms = model['fieldModels']; del model['fieldModels'] + cms = model['cardModels']; del model['cardModels'] + self.applyDict(local, model) + self.mergeFieldModels(local, fms) + self.mergeCardModels(local, cms) + + def getModel(self, id, create=True): + "Return a local model with same ID, or create." + for l in self.deck.models: + if l.id == id: + return l + if not create: + return + m = Model() + self.deck.models.append(m) + return m + + def mergeFieldModels(self, model, fms): + ids = [] + for fm in fms: + local = self.getFieldModel(model, fm) + self.applyDict(local, fm) + ids.append(fm['id']) + for fm in model.fieldModels: + if fm.id not in ids: + self.deck.deleteFieldModel(model, fm) + + def getFieldModel(self, model, remote): + for fm in model.fieldModels: + if fm.id == remote['id']: + return fm + fm = FieldModel() + model.addFieldModel(fm) + return fm + + def mergeCardModels(self, model, cms): + ids = [] + for cm in cms: + local = self.getCardModel(model, cm) + self.applyDict(local, cm) + ids.append(cm['id']) + for cm in model.cardModels: + if cm.id not in ids: + self.deck.deleteCardModel(model, cm) + + def getCardModel(self, model, remote): + for cm in model.cardModels: + if cm.id == remote['id']: + return cm + cm = CardModel() + model.addCardModel(cm) + return cm + + def deleteModels(self, ids): + for id in ids: + model = self.getModel(id, create=False) + if model: + self.deck.deleteModel(model) + + # Facts + ########################################################################## + + def getFacts(self, ids): + factIds = ids2str(ids) + return { + 'facts': self.realTuples(self.deck.s.all(""" +select id, modelId, created, modified, tags, spaceUntil, lastCardId from facts +where id in %s""" % factIds)), + 'fields': self.realTuples(self.deck.s.all(""" +select id, factId, fieldModelId, ordinal, value from fields +where factId in %s""" % factIds)) + } + + def updateFacts(self, factsdict): + facts = factsdict['facts'] + fields = factsdict['fields'] + if not facts: + return + # update facts first + dlist = [{ + 'id': f[0], + 'modelId': f[1], + 'created': f[2], + 'modified': f[3], + 'tags': f[4], + 'spaceUntil': f[5], + 'lastCardId': f[6] + } for f in facts] + self.deck.s.execute(""" +insert or replace into facts +(id, modelId, created, modified, tags, spaceUntil, lastCardId) +values +(:id, :modelId, :created, :modified, :tags, :spaceUntil, :lastCardId)""", dlist) + # now fields + dlist = [{ + 'id': f[0], + 'factId': f[1], + 'fieldModelId': f[2], + 'ordinal': f[3], + 'value': f[4] + } for f in fields] + # delete local fields since ids may have changed + self.deck.s.execute( + "delete from fields where factId in %s" % + ids2str([f[0] for f in facts])) + # then update + self.deck.s.execute(""" +insert or replace into fields +(id, factId, fieldModelId, ordinal, value) +values +(:id, :factId, :fieldModelId, :ordinal, :value)""", dlist) + + def deleteFacts(self, ids): + self.deck.deleteFacts(ids) + + # Cards + ########################################################################## + + def getCards(self, ids): + return self.realTuples(self.deck.s.all(""" +select id, factId, cardModelId, created, modified, tags, ordinal, +priority, interval, lastInterval, due, lastDue, factor, +firstAnswered, reps, successive, averageTime, reviewTime, youngEase0, +youngEase1, youngEase2, youngEase3, youngEase4, matureEase0, +matureEase1, matureEase2, matureEase3, matureEase4, yesCount, noCount, +question, answer, lastFactor, spaceUntil, isDue, type, combinedDue +from cards where id in %s""" % ids2str(ids))) + + def updateCards(self, cards): + if not cards: + return + dlist = [{'id': c[0], + 'factId': c[1], + 'cardModelId': c[2], + 'created': c[3], + 'modified': c[4], + 'tags': c[5], + 'ordinal': c[6], + 'priority': c[7], + 'interval': c[8], + 'lastInterval': c[9], + 'due': c[10], + 'lastDue': c[11], + 'factor': c[12], + 'firstAnswered': c[13], + 'reps': c[14], + 'successive': c[15], + 'averageTime': c[16], + 'reviewTime': c[17], + 'youngEase0': c[18], + 'youngEase1': c[19], + 'youngEase2': c[20], + 'youngEase3': c[21], + 'youngEase4': c[22], + 'matureEase0': c[23], + 'matureEase1': c[24], + 'matureEase2': c[25], + 'matureEase3': c[26], + 'matureEase4': c[27], + 'yesCount': c[28], + 'noCount': c[29], + 'question': c[30], + 'answer': c[31], + 'lastFactor': c[32], + 'spaceUntil': c[33], + 'isDue': c[34], + 'type': c[35], + 'combinedDue': c[36], + } for c in cards] + self.deck.s.execute(""" +insert or replace into cards +(id, factId, cardModelId, created, modified, tags, ordinal, +priority, interval, lastInterval, due, lastDue, factor, +firstAnswered, reps, successive, averageTime, reviewTime, youngEase0, +youngEase1, youngEase2, youngEase3, youngEase4, matureEase0, +matureEase1, matureEase2, matureEase3, matureEase4, yesCount, noCount, +question, answer, lastFactor, spaceUntil, isDue, type, combinedDue, +relativeDelay) +values +(:id, :factId, :cardModelId, :created, :modified, :tags, :ordinal, +:priority, :interval, :lastInterval, :due, :lastDue, :factor, +:firstAnswered, :reps, :successive, :averageTime, :reviewTime, :youngEase0, +:youngEase1, :youngEase2, :youngEase3, :youngEase4, :matureEase0, +:matureEase1, :matureEase2, :matureEase3, :matureEase4, :yesCount, +:noCount, :question, :answer, :lastFactor, :spaceUntil, :isDue, +:type, :combinedDue, 0)""", dlist) + + def deleteCards(self, ids): + self.deck.deleteCards(ids) + + # Deck/stats/history + ########################################################################## + + def bundleDeck(self): + d = self.dictFromObj(self.deck) + del d['Session'] + del d['engine'] + del d['s'] + del d['path'] + del d['syncName'] + del d['version'] + # these may be deleted before bundling + if 'models' in d: del d['models'] + if 'currentModel' in d: del d['currentModel'] + return d + + def updateDeck(self, deck): + self.applyDict(self.deck, deck) + self.deck.lastSync = self.deck.modified + + def bundleStats(self): + def bundleStat(stat): + s = self.dictFromObj(stat) + s['day'] = s['day'].toordinal() + del s['id'] + return s + lastDay = date.fromtimestamp(self.deck.lastSync) + ids = self.deck.s.column0( + "select id from stats where type = 1 and day >= :day", day=lastDay) + stat = Stats() + def statFromId(id): + stat.fromDB(self.deck.s, id) + return stat + stats = { + 'global': bundleStat(self.deck._globalStats), + 'daily': [bundleStat(statFromId(id)) for id in ids], + } + return stats + + def updateStats(self, stats): + stats['global']['day'] = date.fromordinal(stats['global']['day']) + self.applyDict(self.deck._globalStats, stats['global']) + self.deck._globalStats.toDB(self.deck.s) + for record in stats['daily']: + record['day'] = date.fromordinal(record['day']) + stat = Stats() + id = self.deck.s.scalar("select id from stats where " + "type = :type and day = :day", + type=1, day=record['day']) + if id: + stat.fromDB(self.deck.s, id) + else: + stat.create(self.deck.s, 1, record['day']) + self.applyDict(stat, record) + stat.toDB(self.deck.s) + + def bundleHistory(self): + def bundleHist(hist): + h = self.dictFromObj(hist) + del h['id'] + return h + hst = self.deck.s.query(CardHistoryEntry).filter( + CardHistoryEntry.time > self.deck.lastSync) + return [bundleHist(h) for h in hst] + + def updateHistory(self, history): + for h in history: + ent = CardHistoryEntry() + self.applyDict(ent, h) + self.deck.s.save(ent) + + # Media + ########################################################################## + + def getMedia(self, ids): + return [(tuple(row), + base64.b64encode(self.getMediaData(row[1]))) + for row in self.deck.s.all(""" +select * from media where id in %s""" % ids2str(ids))] + + def getMediaData(self, fname): + try: + return open(self.mediaPath(fname), "rb").read() + except (OSError, IOError): + return "" + + def updateMedia(self, media): + meta = [] + for (m, data) in media: + if not data: + continue + # ensure media is correctly checksummed and sized + fname = m[1] + size = m[2] + data = base64.b64decode(data) + assert len(data) == size + assert checksum(data) == os.path.splitext(fname)[0] + # write it out + self.addMediaFile(m, data) + # build meta + meta.append({ + 'id': m[0], + 'filename': m[1], + 'size': m[2], + 'created': m[3], + 'originalPath': m[4], + 'description': m[5]}) + # apply metadata + if meta: + self.deck.s.statements(""" +insert or replace into media (id, filename, size, created, +originalPath, description) +values (:id, :filename, :size, :created, :originalPath, +:description)""", meta) + + def deleteMedia(self, ids): + sids = ids2str(ids) + files = self.deck.s.column0( + "select filename from media where id in %s" % sids) + self.deck.s.statement(""" +insert into mediaDeleted +select id, :now from media +where media.id in %s""" % sids, now=time.time()) + self.deck.s.execute( + "delete from media where id in %s" % sids) + for file in files: + self.deleteMediaFile(file) + + # the following routines are reimplemented by the anki server so that + # media can be shared and accounted + + def addMediaFile(self, meta, decodedData): + fname = meta[1] + path = self.mediaPath(fname) + exists = os.path.exists(path) + if not exists: + open(path, "wb").write(decodedData) + + def deleteMediaFile(self, file): + os.unlink(self.mediaPath(file)) + + def mediaPath(self, path): + "Return the path to store media in. Defaults to the deck media dir." + return os.path.join(self.deck.mediaDir(create=True), path) + + # Tools + ########################################################################## + + def modified(self): + return self.deck.modified + + def _lastSync(self): + return self.deck.lastSync + + def unstuff(self, data): + "Uncompress and convert to unicode." + return simplejson.loads(zlib.decompress(data)) + + def stuff(self, data): + "Convert into UTF-8 and compress." + return zlib.compress(simplejson.dumps(data)) + + def dictFromObj(self, obj): + "Return a dict representing OBJ without any hidden db fields." + return dict([(k,v) for (k,v) in obj.__dict__.items() + if not k.startswith("_")]) + + def applyDict(self, obj, dict): + "Apply each element in DICT to OBJ in a way the ORM notices." + for (k,v) in dict.items(): + setattr(obj, k, v) + + def realTuples(self, result): + "Convert an SQLAlchemy response into a list of real tuples." + return [tuple(x) for x in result] + + def getObjsFromKey(self, ids, key): + return getattr(self, "get" + key.capitalize())(ids) + + def deleteObjsFromKey(self, ids, key): + return getattr(self, "delete" + key.capitalize())(ids) + + def updateObjsFromKey(self, ids, key): + return getattr(self, "update" + key.capitalize())(ids) + +# Local syncing +########################################################################## + +standardKeys = ("models", "facts", "cards") + +class SyncServer(SyncTools): + + def __init__(self, deck=None): + SyncTools.__init__(self, deck) + self.mediaSupported = True + + def keys(self): + if self.mediaSupported: + return standardKeys + ("media",) + return standardKeys + +class SyncClient(SyncTools): + + def keys(self): + if self.server.mediaSupported: + return standardKeys + ("media",) + return standardKeys + +# HTTP proxy: act as a server and direct requests to the real server +########################################################################## + +class HttpSyncServerProxy(SyncServer): + + def __init__(self, user, passwd): + SyncServer.__init__(self) + self.decks = None + self.deckName = None + self.username = user + self.password = passwd + self.syncURL="http://anki.ichi2.net/sync/" + #self.syncURL="http://anki.ichi2.net:5001/sync/" + #self.syncURL="http://localhost/sync/" + self.protocolVersion = 2 + + def connect(self, clientVersion=""): + "Check auth, protocol & grab deck list." + if not self.decks: + d = self.runCmd("getDecks", + libanki=anki.version, + client=clientVersion) + if d['status'] != "OK": + raise SyncError(type="authFailed", status=d['status']) + self.mediaSupported = d['mediaSupported'] + self.decks = d['decks'] + self.timestamp = d['timestamp'] + + def hasDeck(self, deckName): + self.connect() + return deckName in self.decks.keys() + + def availableDecks(self): + self.connect() + return self.decks.keys() + + def createDeck(self, deckName): + ret = self.runCmd("createDeck", name=deckName.encode("utf-8")) + if not ret or ret['status'] != "OK": + raise SyncError(type="createFailed") + self.decks[deckName] = [0, 0] + + def summary(self, lastSync): + return self.runCmd("summary", + lastSync=self.stuff(lastSync)) + + def modified(self): + self.connect() + return self.decks[self.deckName][0] + + def _lastSync(self): + self.connect() + return self.decks[self.deckName][1] + + def applyPayload(self, payload): + return self.runCmd("applyPayload", + payload=self.stuff(payload)) + + def runCmd(self, action, **args): + data = {"p": self.password, + "u": self.username} + if self.deckName: + data['d'] = self.deckName.encode("utf-8") + else: + data['d'] = None + data.update(args) + data = urllib.urlencode(data) + try: + f = urllib2.urlopen(self.syncURL + action, data) + except (urllib2.URLError, socket.error, socket.timeout): + raise SyncError(type="noResponse") + ret = f.read() + if not ret: + raise SyncError(type="noResponse") + return self.unstuff(ret) + +# HTTP server: respond to proxy requests and return data +########################################################################## + +class HttpSyncServer(SyncServer): + def __init__(self): + SyncServer.__init__(self) + self.protocolVersion = 2 + self.decks = {} + self.deck = None + + def summary(self, lastSync): + return self.stuff(SyncServer.summary( + self, self.unstuff(lastSync))) + + def applyPayload(self, payload): + return self.stuff(SyncServer.applyPayload(self, + self.unstuff(payload))) + + def getDecks(self, libanki, client): + return self.stuff({ + "status": "OK", + "decks": self.decks, + "mediaSupported": self.mediaSupported, + "timestamp": time.time(), + }) + + def createDeck(self, name): + "Create a deck on the server. Not implemented." + return self.stuff("OK") diff --git a/anki/utils.py b/anki/utils.py new file mode 100644 index 000000000..54c405f28 --- /dev/null +++ b/anki/utils.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +"""\ +Miscellaneous utilities +============================== +""" +__docformat__ = 'restructuredtext' + +import re, os, random, time + +from anki.db import * +from anki.lang import _, ngettext + +timeTable = { + "years": lambda n: ngettext("%s year", "%s years", n), + "months": lambda n: ngettext("%s month", "%s months", n), + "days": lambda n: ngettext("%s day", "%s days", n), + "hours": lambda n: ngettext("%s hour", "%s hours", n), + "minutes": lambda n: ngettext("%s minute", "%s minutes", n), + "seconds": lambda n: ngettext("%s second", "%s seconds", n), + } + +def fmtTimeSpan(time, pad=0, point=0): + "Return a string representing a time span (eg '2 days')." + (type, point) = optimalPeriod(time, point) + time = convertSecondsTo(time, type) + fmt = timeTable[type](_pluralCount(round(time, point))) + timestr = "%(a)d.%(b)df" % {'a': pad, 'b': point} + return ("%" + (fmt % timestr)) % time + +def fmtTimeSpanPair(time1, time2): + (type, point) = optimalPeriod(time1, 0) + time1 = convertSecondsTo(time1, type) + time2 = convertSecondsTo(time2, type) + # a pair is always should always be read as plural + fmt = timeTable[type](2) + timestr = "%(a)d.%(b)df" % {'a': 0, 'b': point} + finalstr = "%s-%s" % ( + ('%' + timestr) % time1, + ('%' + timestr) % time2) + return fmt % finalstr + +def optimalPeriod(time, point): + if abs(time) < 60: + type = "seconds" + point -= 1 + elif abs(time) < 3599: + type = "minutes" + elif abs(time) < 60 * 60 * 24: + type = "hours" + elif abs(time) < 60 * 60 * 24 * 30: + type = "days" + elif abs(time) < 60 * 60 * 24 * 365: + type = "months" + point += 1 + else: + type = "years" + point += 1 + return (type, max(point, 0)) + +def convertSecondsTo(seconds, type): + if type == "seconds": + return seconds + elif type == "minutes": + return seconds / 60.0 + elif type == "hours": + return seconds / 3600.0 + elif type == "days": + return seconds / 86400.0 + elif type == "months": + return seconds / 2592000.0 + elif type == "years": + return seconds / 31536000.0 + assert False + +def _pluralCount(time): + if round(time, 1) == 1: + return 1 + return 2 + +def mergeTags(*args): + "Merge tag lists into a single string." + return ", ".join(set(parseTags(",".join(args)))) + +def parseTags(tags): + "Parse a string and return a list of tags." + tags = tags.split(",") + tags = [tag.strip() for tag in tags if tag.strip()] + return tags + +def findTag(tag, tags): + "True if TAG is in TAGS. Ignore case." + return tag.lower() in [t.lower() for t in tags] + +def addTags(tagstr, tags): + "Add tag if doesn't exist." + currentTags = parseTags(tags) + for tag in parseTags(tagstr): + if not findTag(tag, currentTags): + currentTags.append(tag) + return u", ".join(currentTags) + +def deleteTags(tagstr, tags): + "Delete tag if exists." + currentTags = parseTags(tags) + for tag in parseTags(tagstr): + try: + currentTags.remove(tag) + except ValueError: + pass + return u", ".join(currentTags) + +def stripHTML(s): + s = re.sub("<.*?>", "", s) + s = s.replace("<", "<") + s = s.replace(">", ">") + return s + +def tidyHTML(html): + "Remove cruft like body tags and return just the important part." + # contents of body - no head or html tags + html = re.sub(".*(.*)", + "\\1", html.replace("\n", u"")) + # strip superfluous Qt formatting + html = re.sub("margin-top:\d+px; margin-bottom:\d+px; margin-left:\d+px; " + "margin-right:\d+px; -qt-block-indent:0; " + "text-indent:0px;", "", html) + html = re.sub("-qt-paragraph-type:empty;", "", html) + # collapse multiple spaces into one + html = re.sub(" +", " ", html) + # strip leading space in style statements, and remove if no contents + html = re.sub('style=" ', 'style="', html) + html = re.sub(' style=""', "", html) + # convert P tags into SPAN and/or BR + html = re.sub('(.*?)

    ', u'\\2
    ', html) + html = re.sub('

    (.*?)

    ', u'\\1
    ', html) + html = re.sub('
    $', u'', html) + # remove leading or trailing whitespace + html = re.sub('^ +', u'', html) + html = re.sub(' +$', u'', html) + return html + +def genID(static=[]): + "Generate a random, unique 64bit ID." + # 23 bits of randomness, 41 bits of current time + # random rather than a counter to ensure efficient btree + t = long(time.time()*1000) + if not static: + static.extend([t, {}]) + else: + if static[0] != t: + static[0] = t + static[1] = {} + while 1: + rand = random.getrandbits(23) + if rand not in static[1]: + static[1][rand] = True + break + x = rand << 41 | t + # turn into a signed long + if x >= 9223372036854775808L: + x -= 18446744073709551616L + return x + +def ids2str(ids): + """Given a list of integers, return a string '(int1,int2,.)' + +The caller is responsible for ensuring only integers are provided. +This is safe if you use sqlite primary key columns, which are guaranteed +to be integers.""" + return "(%s)" % ",".join([str(i) for i in ids]) diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 000000000..38c09c624 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,228 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c5" +DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', +} + +import sys, os + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + from md5 import md5 + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + try: + import setuptools + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + except ImportError: + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + + import pkg_resources + try: + pkg_resources.require("setuptools>="+version) + + except pkg_resources.VersionConflict, e: + # XXX could we install in a subprocess here? + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first.\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + # tell the user to uninstall obsolete version + use_setuptools(version) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + + + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + from md5 import md5 + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..60710e43c --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +from ez_setup import use_setuptools +use_setuptools() +from setuptools import setup, find_packages +import sys, os + +import anki + +setup(name='anki', + version=anki.version, + description='An intelligent spaced-repetition memory training library', + long_description="", + # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Education', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + keywords='', + author='Damien Elmes', + author_email='anki@ichi2.net', + url='http://ichi2.net/anki/index.html', + license='GPLv3', + packages=find_packages(), + package_data={'anki': + ['samples/*','locale/*/*/*']}, + include_package_data=True, + zip_safe=False, + install_requires=[ + ], + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/importing/test.mem b/tests/importing/test.mem new file mode 100644 index 000000000..71668e56b --- /dev/null +++ b/tests/importing/test.mem @@ -0,0 +1,219 @@ +--- Mnemosyne Data Base --- Format Version 1 --- +(lp1 +(imnemosyne.core.mnemosyne_core +StartTime +p2 +(dp3 +S'time' +p4 +F1183141800 +sba(lp5 +(imnemosyne.core.mnemosyne_core +Category +p6 +(dp7 +S'active' +p8 +I01 +sS'name' +p9 +V +p10 +sba(imnemosyne.core.mnemosyne_core +Category +p11 +(dp12 +S'active' +p13 +I01 +sS'name' +p14 +Vfoo +p15 +sba(imnemosyne.core.mnemosyne_core +Category +p16 +(dp17 +g13 +I01 +sg14 +Vbaz, quux +p18 +sbaa(lp19 +(imnemosyne.core.mnemosyne_core +Item +p20 +(dp21 +S'a' +Vbar +p22 +sS'last_rep' +p23 +L34L +sS'ret_reps' +p24 +I0 +sS'cat' +p25 +g16 +sS'q' +Vfoo +p26 +sS'grade' +p27 +I0 +sS'acq_reps' +p28 +I1 +sS'ret_reps_since_lapse' +p29 +I0 +sS'easiness' +p30 +F2.5 +sS'lapses' +p31 +I0 +sS'acq_reps_since_lapse' +p32 +I1 +sS'next_rep' +p33 +L34L +sS'id' +p34 +S'9f401476' +p35 +sba(imnemosyne.core.mnemosyne_core +Item +p36 +(dp37 +S'a' +Vfoo +p38 +sg23 +L34L +sg24 +I0 +sg25 +g6 +sS'q' +Vbar +p39 +sg27 +I0 +sg28 +I1 +sg29 +I0 +sg30 +F2.5 +sg31 +I0 +sg32 +I1 +sg33 +L34L +sg34 +S'a869958d' +p40 +sba(imnemosyne.core.mnemosyne_core +Item +p41 +(dp42 +S'a' +Vquux +p43 +sg23 +L34L +sg24 +I0 +sg25 +g11 +sS'q' +Vbaz +p44 +sg27 +I5 +sg28 +I2 +sg29 +I0 +sg30 +F2.5 +sg31 +I0 +sg32 +I2 +sg33 +L35L +sg34 +S'74651aa3' +p45 +sba(imnemosyne.core.mnemosyne_core +Item +p46 +(dp47 +S'a' +Vbaz +p48 +sg23 +L34L +sg24 +I0 +sg25 +g6 +sS'q' +Vquux +p49 +sg27 +I0 +sg28 +I1 +sg29 +I0 +sg30 +F2.5 +sg31 +I0 +sg32 +I1 +sg33 +L34L +sg34 +S'0bd8f10b' +p50 +sba(imnemosyne.core.mnemosyne_core +Item +p51 +(dp52 +S'a' +Vcard +p53 +sg23 +L34L +sg24 +I0 +sg25 +g11 +sS'q' +Vonesided +p54 +sg27 +I0 +sg28 +I1 +sg29 +I0 +sg30 +F2.5 +sg31 +I0 +sg32 +I1 +sg33 +L34L +sg34 +S'bb503cf1' +p55 +sbaa. \ No newline at end of file diff --git a/tests/importing/test03.anki b/tests/importing/test03.anki new file mode 100644 index 0000000000000000000000000000000000000000..5f285ee0013c81942e3a4eac816365db0db2578d GIT binary patch literal 3933 zcmZ`+dw3LA6%S!I*-R3Og+^OZhXm+gAp~klo3;{2$d(ORLX(R@vLv%RcXP(f&g_{- zl7&nQDpc@QAE>R`Dn9G0iVu9YzN)S7R$HG{to2b#D?UE;PtTpl?(%(gzy0>xd+wcc z&OPUMe&@`Q#)4|j>fIVI%u0je2bZirij7JrE~vKVNCWgM;Zl+(d`v1VW%31gTrY8K zN^gJosZ~?wo;l6hifX~N>;X{C8IH~AZTH+EK6kQ|Zo1r_Qw>ZFH?XARs;+}AK1vE} zaV=Lh0!^*7x70E;Z1b^QVM{CA!DS?CG%dVjxWQw|ishLzxZKC*DL^|ANLM&sp}-wS zpW{Ylz_M*#aPd;1%jEO8!pD_$hg;P-ZmTmqFnF0z+Ei9;Zo1e3jjXv~bJgV?tYYO9pFHUd!Z$t25$si2)5<1(W@i|>}b<0FKeg7kOPu#a==Q@@o+uXp_ zwWI`sFf$`!Pr^=0PnVBtD5@&gik4kcU0j=G&1&?29ZRiMcBqBf^$uRi5~fP|Q&{_u zZJDm@T5_JYa6NSVesZTLB~V1VyMeR{J3?tuXva;^ z0arm3*WSUqSjy(;=^|u3Iou3UM0*DxWX+oHlnu3lTYS7)VNGffTYQ<1y$X|xq-{to z=|%;wVae^bPN>?-lB%K4n0T#-wKy@RAL4l3@XGM~fMr;ojeV@St0(y2^&U%zEa444 z-l)JeuodVu^Z{+OaZPvkdfW+e@Nyrw(Sa;nm-Sc*V{yNa+r#I`X6;!?JR5=mclbCE zKS|)F6w#dqGe`C=p(JB9K|3iy7$s;3u4Kz&8@tR%QThaHHlh;zmJINQlYv{n4a345ONapA!9X@|*+eH( zo{!TC;cq`s8LKe}ubsL#zWcfJM_Ee1LC_rLvtb2SYq~(FN(MZLS`IlRR9H<;CRUj$_;!sloB2FOAv}!yD#F6E?CD9=e6Jni6kRQdQG4f=3 zVg3mb9=qy}e+r1qfanW|w4g>-KrBT8=n6v?FsVb5U707#$1bphNEEtIWX%)_Y6lhc zayaMZ1@~5Rcu1h>#$)xk2Dx3|TE)Nkf59LZ701OAN747Hmm)EA1Td@hIUQCdt!V;+*vk zoQ{FhJMcIVOTAiQ2~qg?8XsR9mi+KaVi+R0LwL-`;}H!&U)CcMDN>X1Z0c^e9Pm9Vm1j;p^LPtZMt6DNIqy>Q~-MNSMZtvWHdMmX^X1<3pxtLA|Wzp36Z zkhwPtbN{KD8wW0j^xBQL2T6I0B0~LEVhM5TWa!(GIMh?UWUGU)9~1e&6gf!*{x%V# zgS_gJIViyNc7fzm(4|rXv>ZKPsdrE@HF>5^_r!PR@Ll19Orr6_vQ@&1gLmZcPT?n! z1WT!g5tiP&sefc9u2>?1lvf?SAQ!1*L2SzNx(l)zwi>#SM*l@G>z4LLd`KVzAJWYkIjHUcr5j}OmY9%4 zswsJn*wtN`#)~{P?^2%}08JJ>){zZ;mdl-dp-fqKsIzwpcccOtvLg+0XV52UgS_t) z&UbDI{?F@%A#>9qz({1um1zjFN#NQ#DHgX19Cch0b>n;LvAueEgWg+MF=XW5J~PaUVYsvLun7V9HO{@gOnfr|Oo(lK0k@9&4R3th{Q3iazb* zXD(87rgl_N?q};W+-pb6Bci8HQSRI6gcwI>cHH zYrg9GBRTvj#l%(Z+T{K-($-qyTJ_MQIed&=D$*BcFM8McdF@@NDVXy%<1a=gXwnZB zJ^0JoVhjFiGc{OW$XpVWD+-VVXH(n`I z^Y|^SfaTD38~%=l+yF{LV#qyLg%j0HhBZJ(w$>Vx0ng43g|7y`X9@AjrGVS7%qCNaE>cmFai4$>qm|pNtv6NFW z3t91M@u!Id!I@K{}&&hVv<0GND)=^U&HsQUel&ire^J?$pN3v z;dzz}=01D|_OnEp2p6B7xSW)30;|L~5t~}Z9N!e0=ngLPH!&0!W@G$aprD1f8?`9A zrM$uM?`+v5A^)&u6}%GBrjP$%?Lh=a>HRM_@=w-38E+TJQRl$sEoC=%nBbukZTr$> cncgBDw_@1oC8xA!XHe{3Ttelf+sgWr}v}AgH{$8YM5>)Ss%asXrFU&Cet$i3Ab} zoUH_YX9GOhemUH(xD;-eTnx91({Nj}3T_z{ZmUypTSZ#b2bGk%S5fz>e-8=Cl}I3w zz?n;+M6OiGCX&gJ#h0?ZxA%r^z1z3-?I1hO_ISom;9CPj*;-+XrZhbK=t zpa1ZC+LUH6i^!NYJ-k3gPEGmD)dN$R@kUk zuiD*N&8Sdoyf$M|+mE7V^51NB%4C+M*Nh&z2hwJ0W(6TNJFA(D8fbO0s%Dx_zGjDCz~Ct zfensQ;AFUBmMB47v^Gj7nPy;pKurJv*(qj@va&6h0Fl?Bl7NizI4e(Jr7$3aQLV!3 zu&jZgdt~kId{H#@)Lp#)D?d>1Z>3zv3nY`v<;WlR+1imRvuw?__$}2xTq=%~=t%J+ z`%9E3(XQ+oy+Q{DM@oB3!*prx zn2qR@EDy*Jl!gcTyZU;&>GnbZL*8Wf>TG6dxQcGsrdEcYcP6mIKwv(cLW9keG>i4d zXZOWtZ;#KuHa>etL^gD1V=Rt17a)o}7a)p27a)p67a)pA7!&Izi=)zmh$7R2h@#Vj zh$7U3h@kY~@W6rM@ZI#b(%rO6uN1Nc-v3j#EAW?Gi3C2l1dev6GP$-ka{O98_)gX; ztZIq78W?^>TFm&HuYe*OsJH_Ij>%_ORKiV2(^+S+ZQ(767bqu9>-Oz1as~!@dvC3g_&qZO-{-MD`Bf~{d zUepoR=tQ3G85%AP>>b2T-X&#Npu?p-rQy;Rn~hZ)v@VKehyt$IK=&Hz}OjR&68myg&FyA)ogS&d~=8~|PdGl*4d^oGiwXZ1yA zZ4b@LmuuCkHf1ei5;d-mjUP1i8aNOdq7DN(K0nDSaoN?{OiZ>Vdrq?@kcUZBKQw_M zv%*Z*W(k(AgTi><}jF0V<4dR7q#$!`j>{6YyYQGwxV7Kx8zDB z@P8nI`RhOPoo{iUl?{bwIe)3YJnd!5Uw` zxtCvYzvg$o^yCAAgAU+e)BI|(d2a{{u0B3!)++Tfa!`c(y$oD|w#y384^B_?qE2xz zoHn4lGK>?K*dSAa6PK|#+JkvQ|39oK@OMF6$E&L{xlNnM$<5rxK))r|)0X$`qJXuL z_cHP^vq^9R>xNbh9Z&OoNc33(fnobBriI{~gG#0!85-JODh>v)O*>01nx>LkR7Ta8 zf%eEWt1L>9LcP3*A_YK5foQ~q7AG^B z3zIn!MX`!*-|mPx6!T($<`NL17%holcGKfan*p+2$LoJJ^-V>6DD_RX_ktJ!5+V`_ zEMEfb)t`UyNsMpKtpiQme&|}57Pl!%8%cTQ0}o>smkeI^IQkPDWH5Fb5a_HKKnP6c z%HuV0Uw_Bv<{#z7HF$B;El?a1OI&Nk(a=T2;yMl?{>QBrL<))lB8y{nhzWILP*Vow z0MYufZ@=;iw<|dMO?MZN8v@+8mV{@@6nTshF?exJf8g{s&G>}w@?^mPU1m^*U}T@e z59k<9|WXxp$bN3G-(FV>*A3Z)fI{nH4bJpVLTVQ=ICW#s-da0H&ZY1OK_0#O><+`$tMM)yG#zjX@3Se%z@L2ah4uXN z%a3hD2CnfJvIx{E#2mVBhl2Va7X2qzB7sB#{|_aA{eQLcilV$i9!X`?-Rj@25FyOp zkjdn>Y$5lHbtv4Z+|8;CR*(3VWYH2T$=?aRFZ*tghe)mt2MyT$6!? z0Lp+(#tU}VWK3U_$$fYWnZHbjAnmL^d)QAE|1l8YVV%Rnz~JuET{Iv5K%Nc_(#8Bq z?h5zcxQ4IVIX2|Y%`86ovMvosNTkJC=MHz$+ZPm=H=m1=X`G)HBCGX~6*@~>)$tPN zHgzC;79cZFPkAp3R#S>Kbb1!*tBuUJxb=k#rme#sDAX5Ex~#dTnn8I@33%G&D-WB@ z(8cN8HEl#+R1z1Ac;xax5!e<)2|{+z1|(*lP{?9;PxSu^8T1l15(%7{1W^B{)gDEC zS{+jVN&QpRf*;A1NFb3why-$f`OqU`4xU!hY0~oQ@86Ac^U1G2+>Xoc=29Eq`|7Kw z`6QhKv`T>OtA57U<&o+7?8eW3`U^fJF3dxt-{UaK5p6E_*=PUz9dr|%5CvWJasl$L zSnvw^#h<)}1vvn%62L9!h-Pm5?bG*hm<2(MgmDWRt<_Nfr_~b*?8tvYeMUV2JMte5 zVLVAnB#=npJW8NdO{Zn&-hcBKptHA6Vw&vFzxvK=Zkp`BySA-y)8w<;a=*rb;)MU% z7w&w^O_R_5=8e^uwjlcdH|n;EKfkXmlDS?aCmb4rjE>T<4 zU`xnh0e^zoAV&m*lp}%2GUXtEKSA#Y`#3bfcjca z7Gxjyr^$})Pm{eJ)1+-Ld%ZtRc71=E?Efve_MZ$X=6kRkYzTpvUN^H)6eFYn>R z?bJvfW{9|NtF@hEVCNR!yA?biM%LMvp1A{Tv2}Aiep@NLgP$|+fBGqy2%0cdMyD#g=pfBtg=|G7^9Z#}ka)j#6(ynI;TiV<;k@** z^zvy4$msNQ&-2nwKlio*g*#+O8csid&(bu^eQDu+q~X*(OVc3hDIkZ+TYn^~;H|F^ zr;?z$Bn_2+ziMe3KrqLe+Db%SNE!liaN3Gl+1l!FD^6WhwjNs+4NAz_7jo%$^FAlB zz`8ko%Gq-j8u!tieQ~k7p>esA+>REzx~c*}ptx<-h-f#iw2J}4h!CutjDg_W{`PRN zSWFP1F)e2mH-p{kF(@O?2(jwrG9HTqOGCrUZ~>x@t8f9L zPOESMA`Yv_d&gNsQRzWMk?BE1(dj`%5$ZuiQQBZuKO9tIyL%jhQBR{5lY8!?m+pp` z#X;uaRvCBy-)eH5LarkZraqZ^Nj=##Tqg-l61a11I=8Kj)E9+eMVMIxSX%>eSRRyj zLcJpTkKuS3igm;ApJFY1h3sbv?&c@H@5x#p2*NuHI3qV$OMKyjiNAS-LY&tnv~GMK zgt}q)5(sDK$h4FrXUtjQz`y63r056Xk#e4ZfpTY6#rzHH{0l#LwzHzc|NY8?9`+a5tMccY7b4s$DZ1 zfgc-#PNG0w+<64jy&~OfxWmMT^S|SQ13ccatoR2UH8;DwNC_O;7amc7jZ1Kdc}%Ec z+0hTAWf@Tt9{PwY&mPx}2{`#`$-Pv0&)FV_&-J;-04?o$7e2S(RxR#Ib3AtQw_eU$ z()~B_mc-B7k#5AmkE?<$DUc^Nn(?G_>~NGg10F|Fw!;AGe?^9lm)Cm|s6+yZ1XhRy zaQ>h4{}oc12^)z7&QJoV|5ehekXCXsb;B7xyfVROTQj*^I>~&Sj7kTLwZo206-ex8 z3d9Do~tb;LMnD1Jj*#IY`-QR{U0`d-mhEuZSnb4Nv6m{o7>7&$_PBklQ z!Epp-n{K6}W^EGQf(6Gsbps3>X+USjnaQ#|sYgW0=`A9wN3ZagcDW0nN&s-Qv>~&hqk|kjjHJTEs2@v{p8+CeqK`sBM+lu+K_{mf2m>1ZB!|$sZ5zeq zVE8^Td?^<{pG)wA$v4AGp3G64d3AMeu{yhHr%{{fEO0zs-tk!cjaqd$g12U7DPF!T zNdXWeb44lq6tJig3Rxh`V(tR64oCcov&jVwT*SB1UOdH5<^a<>`13|h za1SpYv&Si%Ozwen+0KhlG*B9inFh#)`8a z3%ojop&(qg1x_t8PABVdd^wI&@SZ+65>#}~520I$zpSDPE+|fKKvoD3z+KMh?nsa~ Q8{haZ7+vmza!fA#FLFWq(f|Me literal 0 HcmV?d00001 diff --git a/tests/importing/test10.anki b/tests/importing/test10.anki new file mode 100644 index 0000000000000000000000000000000000000000..dc881408e244fae20f2f238890d6859ed0b43b9e GIT binary patch literal 31744 zcmeHQeQX@X72my0?#Klb5+__7$IaxT$iCoMc9Ie(N%K}B&A;?OpU5FZr{q9xD|QKUj8TK=h2Bq$%Hh|oeCQ3@)RP%Bc%9|a_6)%Rw0 z_QSWgc1+^B>8|X(yP3B$Z+^S;-kbMkW_RAaO*IwLS0RkB(wovBS#s$$VM>TVh`?kK_*@lS zSzQUItFDC86|>+GvE}H;50J=rx{`eoFme4L3$2O;SnMbBJf{CU~OcfATAaC zj#Z6~*RE__wX$iAAAf7bau*807h?Yx#kU3c3y(_>fx`>l|NF7izk2z*Cx$K;#8|AR zX7sw6SVT;pe(%}-*N>gFK7aW|xlh&axrh#$7L-?jPAmKK}>fM1)cs@2K#iyFw}z9KsH0#KtazT zrEJD!T_GBpp;-2Su9~Kz5mh7IsxEiQnoNMaM>gdi{EoHc3Rl zWsUR|OmaSWwr#5?@mc0j zpW>U5lZVJAEXPf)D}9D9brPN~c!HG@{Vwp*w3`LCo0%<=C?c$}wmY zlw;8*D90qo#G_FG|Da`1Tv#6{CI8CaW zYN~R6CnF714r6;Rfd-LrBed@g-3`$h`_W%;49)HAr`9;qd0s+&p-r@ii(OQ@Rg3_4fANMY-JT_-Y*zj80Ti55lT(jYA)*xwSY*u8?jK}+K-@IdI(dbjOoRSlS`ON-b zDG2a4?lHPUqK5VHZ&2AzZUOY zCTR?5*;Y^lNh<77%qH`)VeW+FKmncXh7neaW-9stIlo=i&VyJU3?}YXCtk3dcD!;G z*%+EI#~$KwN(NZvp+;ZqX;xGDtW~~Q$!sB?m-~zf45GT#uI?SWS^x*a5W9yS9iQw~ zazWYo!azW_!Fx`rC6I(cR5LsRZF)}8ZJWhgw?$&>d%saN`U}WvxFaDbPB!?FZVsu$iOr@b(F(Fp-f8;{s z%B)?<7P`<>E@&O#%|RERPUQV+9=bR&3*eul`NPbKXUck)V!C{N;_|wT?RHEV%j?>i zUo&OguwP%lyly*ltECJft%Ef_e_10vf_{x`c=d^IvKG`p3zm+|6qjxBX@aefcj$#& zF^ero!})*$u0Ydn3h)n3PxPYpQ8FAhpu5tqB{s9(MzNOI4GZGko5%G3U4j6A)8aUi zpAkzeT`C@1Mr{oATT(r3INvVwSQ|+vBN;H71UIm%$$4LSnxrDpWeIr1_9TXd;GBb- zOm1vz+m=bUdP?iJrdpH?CFOD%c~=J7BmGKlOp08RlI$5IsS<0v+x4DpqKA9xD)9uY$)9rO@Qy!9ZUhM*d$bGd; zx$>>n$ttbX$tzPOBk-vZjX2-xct&YuJf}=mN=`LzwqW)xc`QL`4M>qLkAye7whWov>=^`JS{a&3keW15h5^g1e7=a`tTDN-yEI;nz(xBY8V#7?3XGr;+PLK zjF}!cc-dg-PjHZd?9?F8S?%S`s2+owJSYbR&F^~d%o%D|(BhZwO<`+9XpL=2IHpXR#u%{# zC$8xZoZcjBUsLTi86cpu3e>?Gvejx2>i>w)BM3c_JhtN^9*1t58A~i%CXO~!%}yh* zOtrhd#=W}Tl{0OWdwHL)uJjjG9bC`8y0)mPpi%u5u~zI=bT#XLArw07t6+FWiDm%3 z&Mpa1UA`rv#=(q|5-)_VIW#v)-A6}6_5gsDWOU-`a?eetNdcBzu(fu9oJ;-@f+TwN zm0g;t=7U`}Xgc5!-epswMfBNG#8Nzdf2XDr-@lWU^cVnWFuVQ#5$RDudQ^JtBKG~T z=n#PrfzO1%(zAc~&0S#Vob0_!h+;s%HVLTr|CN{L)6YjwEfSUrw~5FpJLGIo#%q z%?vvDGOqL>#L|MSbE|dS+vf$$8PAo&G{{d2k=3Hl44v_<>R`mFP3;MvdD!&ar=*hw zvnkmeIyDaWt4bWHuE~adwVi(ceE}-B*x)>ze&Lo0@bJj!zmSKMw~WsN?^_-m za%b-)T`0Rbbr+$~Nb7B*$OVj(THCMiIAJtkH7q}W4rz`R0Xo0hhWf4dy6{_w*jrkZ z+w;RFD7RmQO;B#T3Y(zZZWT5`nawJa&UO~%xO6DWvFT8hz<7Ho*=w6fwSxa zYhIqeFmXQ~z7!XA3auUA2T&KZUjgClgxJqvIsu;O; zj(g$<&9-KAxW6y-AWD0Ncj5;Umz$(8$%!9Uk)8S02NK#0J!LBKu!e}C{{7!xJuxzlPI z|A}c)_gH`H67>I9-{MZ|`O#aL)`8>yaQ@Gx4nv(k|%t^V+qxv@lTt$1`hb#}4cueZY9)&+gY^J9_M zRpZGE+7*C1Ik=Mx*Lj$*3wMxnyP_%SxV(ePvpwGdMB_Wandi&9V2V1lEWg>v`g!z8I(R*O zlIX5C+?nV3vDo`0dGgqDFPe0L-G01GkKd2gZ8c!9GL~3ZD~?ogN2rIheO3XU#F`S{ zpC_AN!@Pf#sL9l9Rhj?-Dh_boKz4o0r8f@KWtZpnO!xYCJi#0^-ZpEXIZJXl9< z#s-)^R8KHkY&1CAIi-M<(3}{XeUC!dnWSv!nnxn70VtMu? zsHN*jV|?XGqNuQut4`k+MGJ>1N3>3M5hRiIqz>NZ(~0Afy4vMNZ9S>g3Inw%ay4m6 zjnplSVcQQ@(YD(=L&LUNe3NLy+jb}2)%F!+GEMR}2H&_vhYz%A^etb`q$OzWp*2;p z1vNF|k#)!_jM17AK>q1rk|(-2Wbpa$8E4S5+XXLbTE}io1G@_pdM}>L$!;#<1L zpnU_p^~ncpT27s_Ulx7ERuT5(E$DHAhbxP70L<`QRtw!0jFhP%g-?T_z$?wzd%5V& zf4EmU?D@+!D`z~^QH5NTxocd5 VLSl^6n!HqUlp6>oe95@^2yz&EwPCD9C* zmUVEsEDo3TF}Pe3flDLq$_;!ITtmW7IN?ulVIL*|CV@2}0Wq|Zi*-ajK0V#tz1zF{ zwx{=s{6;QX2gdCG*K)!)g=^PD!5NcG0;^I2BY*hKJx{*)=qq=9E5aQMMfN9hyCwPk zyQRI-q|!Ih-S__Po}4_{msZlfy?uN8_Vn$TEl!0FWzO#2{fY6}6Fa9%1!bqE=%xG# zWv8L&Mh^xcHzU74`wu|<^3zXQsC#$qmU?>jPWH;((nM~z(v$AnHQC*p-qow@>Fe#? zLr`~M)QdZmmugX`ZPaPp|9S2y4*y*!pVKKml1L=@&!-J(A`g<344qs;{iE5;covOk z4j#!Ol7y1+m@1=zq4Dh1*-R4eK=Xw4Nb}!ihMq>qrjl(&0$el$V$4> z9h{vG&h81$-W8m^+b4UeG+&$wV$K2ZW6uHbW6%NcW6=TdV-if~c})r8vI619W(C5J z&kBSeqZJ4rPLGZb3}!}eKp)QDfRd`5il=b@7q|yF?g9Rbp^?=)0+iJd>C>moK3z~` zRU#LG>eD1CpifV#d8G(1qVeqY(9>PLT~}}P?(I2Ek)W#u_S}-DPN_vHe^fH2#ai65 zqUSVq)=*2u+RWNIDen^^iLG1tlY7bdMapzY$t%Ex`#wySTy$rNSOjLufb7$=WD{fE zAp{*B9?cG1J%k%g(!rTRquImR(d&ucMRb4nuclCeDtG9ZG0LB@U0F%H9 zOMvMAp$9qmV;?4gRWE^)+28&4mtYIO_~!6BE*6P2C2r0%MMBurz2WDBjn;SXK}lD0 z88m^v&}3fHr*o1fqXpHNMiQFTN<{-nMH%iVq}r`o^@XBckDSAq|v5$Tp^> zVtii5xvGJ{CY+EAwXhF?u9BA#);)BTS7#I?DvPtAQs}^rh}ntffRJWkKq6rtGJty# zGcR_L`voXyxr( z%FSyUj5&kY1gx^=M18548-kI~w3)F@Bf?m1j6rgsH6%{MXg*Lh6zznRA5>wS4Dzc{ zz|vwca6w%@VmE9(yf9ZREt_KsxDI9ztF+YZDSMKY_Rs3^%o1~@d|sN>Esv#_YgC+8 zr>2i;D)>E&B@c%Ob`u0;=SvF#*}C0AYfW?!T$TM$_rn^@xIT^z3&gRiJqs`?fhS1D zs0OioJz*?F`cXwYBtbDffpl}G2Jh+-wV)coP}tcL0Oo1rQ|_0l&P18@MBT9Mdb8Kp zzvB5nBz%t(z9;;yJ~V@15?~Vee@Otn_JgmTffdGE?}~C^Pzs`)J=>G%|MUBvCEv%t zoJMbb|M@#!eDtN$>%1VB5_9)s$i;7sbHxA8_j7zde@4juze={I>3(K^BUzgG!ZG6U zCf-$YiBkS<%j@d9+_((aHi#*9>asAQcA6*DCSe^=Czm)yjU)v**U4<*(#NR9kY(_) z9Hr6Q%`RfsxN@v$sQF-346A~8hIVm@RmA$Uq+K?@>9NLnUZGUuK(Azp6guoruqWGc zZ^T@%G1okZ><^jCEAIPsR1JylEOWsIr??7BRS7uw$qLYUuGd&7Db@*L=Ko*QgAQY! zNnj-;VCw((a>5hBy(@u_;b0PA5?CP#bUt+Z?XQ!yj%F?vZr#!pixGz%Z|nUB!T&!# z4GR`UeL>N7v?M?K^dIdPqTxjC3y1N_M+VJV%N;vfNdM;^i00)J0a(&x9M$9P__38t&)gLaCDD|*c~yfI zse{AAN3xkAS}&Y0s!*MxXJ&D}plGlH=J_HZz40(Khr?1q&0AhP;)1m^#vmlh@n=yv z{iQstE{W*6%xM2LnbG9l6v>95EW;C{ol%4Ff7j*XsgwI+B;x4i;YtJ0N!{zsRJEIF zK}J!a0$yoOemZ(kdjUuFyhqqseE|e(@Whm%T!a*xl-@5^vLF()693mABsk%7!ViRA zxM3eA0VaXO18?086X`3r-W}$`;aKyZe)6xj(vL2=OYU7D%WKj#4?J<-lMX<1&IZ_U z-E#!s18QLk>00i9xSVYOBHH=EEBl~6LW&WD`?*I20 z;@874(K#EYmg;hL2w=_2QdH+d#$c?2NW=1`$LP z{FU}UmWW&5Mi2}{VsNnX?L2Hv;>VP}HAz(Ll%CNbvbSYRH~IKKoVfdm$6*|SaHcjc z3iM-}+XLtm+?XZ~{GlEkx;7*JB&i(S|9S2X4*#u+&*^+TlIZB*Z`(q&IqYUNRrt!? zrIc1DRx(zNE)RZO^@QT7cF$QyE!1p4ko+?8t_-a1%_(wKid2&#+7{kGF+*%aZ~MRj z^VHgH95}N^bEjVQu?K7fnY=m$F&ftTIWZMXi`A2CC99TbCC`VX3E)8x`|&-=*%`IO z>>M9b*r*w?3~5iy)c~~=Fhqvbk1v8scFpVcZ3o*%rOW`5XfIum`9JhCPWU)n*oR4A zZAqZq@||nQ{-qOa0|B2aK+W1IYg}vo! z_!8I+ghP1!4x2OkJA|v--0muadm<=`4v&V1u%jse2>U8ZS`Y+@?>kJu0x^NFLes6W zQGqRN0YrUw-s9>@3vv@=oF zN&EM5ruyy`hhWOWgj!T&@~T%etUrsNuLsakh`~=u&HS{g?Ck#y@!#e6@3QEhRX+9I z+I$I#{Q1&Nj%B3ds@C5{*+V4hzjkuF+m7d``L>1z1v$`dW1==F*wp!J(`T&!WzTY` zR`hsakH>SK9klH;HbA4M_#aXOR{W3qzLlyc(iwFN%#8oRzIKw@1&@su$kTt z*hscmPqi~4$tG_v!wmwm;UKUegONCpm*MNcyez{fLL41I^kCO{aRxS+*NqVuE2ec2 zKF7ii0IF}k)>A>tA6!pb(#id#C7JGNOE0D#HqtGL$}<&clH_VLbfgqi7(PR(u5{=V zbZnfI3T@K1bm($lbEGwuik?j4^V=#r3-zH=)st0#>jttPK{OnkIS|)@znp>IXP!Jj zwDkA_937w_;$Ap_x)I*>Puvjb07}R4lgqz=K5zgf;b^!?+)a|=RvnH85R0V+Z~|;Y z=~Nu7D>y{}&M$Yvt zlQ&fj#$%;Y(KY7cN1x#sXPw7*?6{daV1)JBm;k~WiFoW-HvB;g8+rGcSC+3S%P7=ngNK;ucGCDhcGYvza6rRP%kM*%{@3mp6= z#|vw`=QH-11egSBB(S~_q8`GbV-Kki!JK-;dK~!CKwIgDFC|$2uMrqaU=p~1B(T1b zk5T=fJktIDEGN7woV|bw&5FV#aFI&ja_H}ouoIJL&-<=q90+Lle{BDUd7a~R{x6|y z=vP8UU?%~;y)9>^)wK2!w6$mYCI)bf88P9m^tSd~U@mKqvd~>dUMhFFNi5<;jF_{8 zSX&S=0_H~y%-2+zOGFZVV89-rso=F9d@y8%&bz(oP=J1vs!V&z@sL{B7)k604pKA+ zaD<=feW&v3CR2n+hc^)u!oiJ_W`bq^Kg$V!7oL54n?1wMB)}x_&Pw3YFi(ts$NF~??D^lc z|3UvhE4=#7^2Vq!2`~w)yaYhe4}-mL?{jkL|2Wy+Uqtu+r#RuS!UgmTfGaOmhLK5N ztw^A?f%k9#R8rmczd8Ru!wJs_=hjO37@tf6Oaj3Y*dT<%)hYl^^Pc~o$Ls&k2V-Qp zOae>-Yfb{q4Y3Zsr@Om%dw1XV^xn|=hG<8u@{H&I25|Wcj|&en9bnDXE@Sv2lYo0( zA366kd??}FR9at?lV;EL51;udK1s02J^vrP^Bd%JL8t$pgAw2vw)S_CH78cwH6{Vv zhwT$V-TmJ_0d&OG|7|#T|F_}j{y)VDj|qk_B|Nmo#KG8Q5?BioSSK`xCA+^K5az;_ zbG(*&KCs=hn}yaeKGVzY)ik!{o(ksf|7`#NT4+LyM<#)lmjI6ci}DRzs3&ww=#Rod z;ZFWhegqOOqR)rLFh0HMV`TfeK9^Sw<+yH0M%sP1kJJDMjg5a}GzT9LJGxw!i zo9B;PTi2 z(u`V^rKQwp8z9}p7i^^FPj%Sh!oSv{Z>h#g!4MGj6c&7TCpb!#zB5>hjpt6C6=>$B?= zKMa`r0j;F>R-^ZT$K1P{XE)7O>#2B#pWt`zIke?WN#*%H@?66gHf+|;}h2Ir#G#FEq$h4Rdj z426)4R0VfW-+=cis(C=eyF6VSLn>~)y>n5hNkOFL;f;@f<8Qv3y^-hZ25$T7z9-F3 z8w%w7-nT#F`78wnZk-JWn7DN~4lrex+&;5@{nl@PnWq|0cxHC@=St%VXE;{(ue{`4 z>~Z%KFQe(|1(PSMypCJB{I2V=JMD=wmp7!edhs4OHFedou4$`QN51qajR;?2_iuW? zt9X#!$ye{@J-_U;)Wb5wcH+tkohmM zVtq*O?lVi*s7*DtNzs))w`%{d3lX+6)+kSWp}S`G+ZP5gOx^1vrv{oH{S*@^y7APp zO|cuqH*y^-s^e2hn%b56uOjeB*4j>!h_a2J4t=`Mub4EoU3BBwHYR?th{NHLo?%kg zX1;p2p~8RDq74?ioi~Z?{XE)7O>#2B#pWu5#3bvTC4^Y>8_`iF1B#Zfaf$gL6@8Vo7R>LV0FM zhC)b2s)D~)y?c=J%$`P)|dZ&&*(Yy zJYd&`BZV@@k6D;AF>N@&#I3X8KobLy0I?JpxYxYeI6eQM#N#u!Mb_=^oz!c=xx{T+ zZ|O|WgWCMB?Sgkk&G7R5b*5@fW%blITW1@UzTRB4#HhYsA^2#-$)L>m<0Z!>Usv!S zEO@Kr5Fa0+U9)h)V)v>qEH?wVuf)$WpIQ?=ZQ;JA<2LF4sD&G(V{(xf6M9L zYc9C?yxII~ZP?cpkxF+pS@(%gJGW%H|0W@?6oJ)GCrvGW#+!7N@%5gNC};7MQ_`>X zSDvY4@?B{>EoY6#G~;PSYqo6o^8QEr%8-*sH#w{-Zq(nyARV`H)!q=aOgso`eysfb;oyCvfX$ep3=HpAmT}@KyH?1Ti*EZ{sufUv01*PBs_6$++c&ej zcRP2xYnwWed!EyIZ+G6?d2im#?3?%I?epN#L#m;mk)oED4YY%6;CP<98zGM4V({nu zY=(;lM*;6R_dRU{Xju3i#)%7`3YUdT!iU0p!k>jd2^Y*_-`g3GNq|XUQ4-kH3g{cw z!D0OxIIL}g!|DhennXBU6NEz}p2`h8h6IFHIpL3RU>_y{CV~G82}ptUT(~Xd^6BpC z>e<@0XKP}&#INT<^Wfn5&kNt<;LAQt0!#wSQ36Nbc;@`McYpf+KQ@Wnv4FTY+TESF zEiv4^XSgeq=ux_E>*?Cj)770A*|k$ql!T&en<$J1PA7l6bLZT*e=)axte97}Yl>dX zom93Pif-({2;_v!f9?$qf9vIQCeDk|Xq5kQ!jOk^&|KNj$sy1?kV+1v&`@&!p%fxT zC>9B;S(NS@O5KwhKz;o~sPEXJL!BrRHfARjS3p*tMF(&_PHat?&|R@5`J|%Bqsow) zS0t$XaB3jk8%uO`qV70VgQO`ZRb_g9ysWAlnw&9|>|$^SfI-Qf!-6uMl69raADr+9 zcl(2P_=9(PfM<(Sg;77|$_QTUl@YucEF*ZaSVr(-5=`eQP4VNhg5kwx1;dNa3WgV> z6$}qfj|`*_CkGxt_oW^{F*O^H#PR$WxEDC?1^#P+qs#XRP*MYAPM2^VkwzuoK#kW{vYcLeRnf zfmHgQK0Ih*`CQ46Y}%DF)k z)mJ%Gj}j~l)Z5=TI5dz1<%KL^jf_ile_1l5x(k=-j`>e{^_K4=fN{noz$CE55+LjU zz{?zb*@sDB*-Kz9_1%A+!?y6n+_hX-6q}-_lTBg(o4TcQ-&tk-_UxB+HIqcc_y>*T zWPL0nYgsg{8e>RCBU-UwAi0o*^I_jp^NV94{5@8+yz`cZ-mpaM$ zw3^EyrJ%!VBm$%i1Xha%1%{?V#kf_3QQd%|ZFl|TS^W7%;JE4kd!7@XU-n(k7-SMy zaT0i#Z{^@A#|OEPDDvk{%i`Ztx5(#1=}S*FWv3_`K5H6)F}(>z@;6z!y(JFLQU($B9(A|?1DL_kmsox#468rxyqh^65gP0SCEt`=5q3cZh0)-T%*#MIy!bl zQ^D_H%(^%{u$#aSoGVWIfOUI>)|%)NxGH<0@B1~-xE_uT3#DvTyEb4%22W5HS9KW6 z*A>UANI#-z2V`ibE0S(%YVfWeQuC_ekA5kI~MOkt{$UcL|_GwYe z;w=$(tx0s1V>L)^#Vn~GUQ;HgR1M7e{r&xiQprAwk~CFNVJ=9nz`|5s(ZGG+dLvZ& z$U&G(2j#q)vz9<){b~aSVLz$77@#1%#T=OC61qP*(0gxkAhtVBz_6obxYlR`YHFN+ zxnPUgd!1nFf7k_?d2<)}cwd}+?d7M>5*5FZ3jzHw3Ej16<6kFz zQa7fF9e>nCnXb)cS2h!kgx$8LLaLlyOY5TwVA-f6 z2+mPYDz2Ji(t6dRCICV90^}S3tSKgyY*mR|QzF?bymTQ+)`spXu8iiYc{DSLj)6mw+6!#189>D#dG0aZFp>fKwf}!U8w_>pV#U6Tx zp8&5N{&%&9YoUVYDwJvh=qezI>J+@{w4Lpxo`@N+X~sMf*;_-!d43($Ly{9@jwoE> zXe`ww5a9P&29#AL#X2GEi2vh--*fO~A0`1Nfx3_YI`!|1zX7kpN0|^8gz$k-*@Hm> z1#W)l9QkcN|2o$HZ2eysoz5D;B(P`+nEwB#IN>GXsYTz+ZeS8%5?CS$-0<8#Uo&-f zkP8OGE#jxgAd;_;`SIf-iRhDy%GAAodGkfQTk%l_ zH-sg${Z7;VFK}FefAPxO|8uurLtYL$-Cx;mHg}r!>nY=i>@kCR<+;C2tS3)fsj@}w zh2pn64KamNE^i3VZ+H3|Tg2+5qPZwGDHE+~lbyU-_w?#(iRJ(GN6R}SI1UK2zMJf~cnR)`x(57u#okP)4GJ`U{{&$|Qqxwm5f=v4#mw3tjGur=FmDJ!Ogem*~VNQ5>i5_FD8YTfI0ZRg< z=r6zjcTm_LlsY;7*}nnIsG05}3p&p)iXJ};58zlSA0(^*i<%z>iSIv1(0pqG1*qxP z-lzhvu=^18%)FsbWimKA8+PJdL${YaF-JL3gjXhwGJ;nYh%$m#5{NQ_R}P3Wf=3Dn zsgfkZi^~dz7n>CfFFq?6UW`^Syf~dz^j=8+;T2O~DEdJcy2+ry>k~Lw1N1eH@%I^L zlB$#O?-mc}nJWffrpOPg1tm*vbsL6DzpTXi-Cn~{ik~bw2tA7UEuS=Mq3`I?^#8nWS%@sTuo+Tx_cq*Tv3~)E~2zcgMtyC zhR4exWF_Kobzt8ZWJ8$W0-47fW12GXmgvlUej5{$WN>g>l|i%xWcl-34Ptv}A%+a* zWdV4^it(cdDJ2TykUe7Fh$5-7ZuYCMynOaSntyFNpMR;eXSLW8kB8W)AL>FlBL-QbqkSk5!B>v8 zH@agVib2}M5j@IbQo9amGo(Us8iIPZphP?Zo=3>A0r@=~_mt!{V#}5-p}9w^cpoel z3l0|Xr3d#IvAw&?WaJU{Fc^TC$_Y_p`r#R(K37mU-|EVW)w=0<52GbU#y31b5=a*KKT%E=@R z@nDsn8{J;4R&j^jH|cCAvfS{c=Cx$}moiTN`teLrvNjvmr5lG9Xh~TFFV$?b7~K`+ zOFLA^Vlku6oFGqw6M0!1Pt8o=cl}7g4Hir3z5}V_$PEp_+$5-x7rWe5hIdGS2#FO)ji{IB;8XYFGWa7bWPBOk7r{~vI|2M!!| z$|S%fP+t;Q2lHMGmIDE3{r@p1d|Y46WvyWnU=pa2z?vXWmj6}jf2RLyB*hAt1nNlw zp#Oug?ziI$ZT*k^|02JO<9G3|1`>hi1D^;f;n{j>_!VeNiHnNSJ>Y`gN4sY6_9sq+ zTyf5}x;tknL@)Pjm2;ljI*3oN7o&TDgE-9rq^vdf4=OC&ENM?Y}KAXJikGV z?gPSWYX}pPikX_1c&3pjWhbgPY3!fwyCE3e66IBzn#mL)(&kRY#3m&aJ?gaM1LyBK=AyrpMq2L-x0g9Q9d-!K?@qvv{=>|S;S8Be!a>68OUTm(e zmBPWbngwM>l~x#H8~9RVb*1><1z6F2<}EjSRqeo}2sp|u|Nli!xOfHT39FJxfJtDP zNT8*G_q6{%-^(BGT z4dFJvyQ`~bYuBEwiQR!!4WYJh<=UM87eN0D7wW6wtTjvmD^~(eUBCBY6U6!yGS0I1 zjy*)m%JcsMvjA4^Xkjg{HwoamZf6d1&VM^o5Iz52;e?+Huhg3iSbLZR>Q4fzg_a=B z>0!_6eZpjL@mWEf^PlDatiMshTErx<5DDP*Kb!vxkrcyW5~x23u=!tqeac$IB(P8k z;P}7BKrb#Y+$RO2?d|+SB=?IxnNtnr1de%2IL~jjqRxhr`wyj% z6B09qbDe5-rW4;2@4pEG_%>7m_^h}$l4IFCZ}*5_Gt)3TBI8Iwe0&oG#F<471F zr0>K3Zi(aqNSg@&tfE`0t9QXK#|c{ZpcqCpqN!ze7voyfjEw^OwtM}bQHy^3A*5u_&>)z|a J{1q#K{{kFhornMc literal 0 HcmV?d00001 diff --git a/tests/syncing/media-tests/2.media/22161b29b0c18e068038021f54eee1ee.png b/tests/syncing/media-tests/2.media/22161b29b0c18e068038021f54eee1ee.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a2ba43fa5d49cf66412d546e9ad4012842d0bf GIT binary patch literal 644 zcmV-~0(Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RT2@we`F{vpBGXMYp9dt!lbVF}# zZDnqB07G(RVRU6=Aa`kWXdqN*WgtgMO;GP-C2jx!0qsddK~#9!?cCdP!ypVr(ZKNk zzwEwcJe?-v#^^{#*}SJV%RnV=<2a574 zQ#IYW85X6FiL1WR`M0@V(?u#Pd5*$*Uz08MY}K+}i8PQ38MA_VN`;8mBXlT|axR#> zvF-x}4F#)TJBkZ0R$T-HqD)OtArWz9e=N2#od zn(5f4vT`8TIruS#p3Hl}LhQ93=H=6B#exxA78n!yaC` z(7|zY!|(TTIZ*yF+{Oyp{5Sl#&KsMjQ|OT135M^z4*zO0TdHXsly*3~^XCVzaOma! zJBSVq^f*kQ+XtUAUFRB}6%k9XucG0{ct1#YG+@JL% ztG2}7bLXEy=ukFu+Vx`n4?1o9gran(NAlR`^u1s}2%n-6!^Pg|A-X=I#nmCw$-f{n zSryV|N{gQlsD*WtEN3CILL_;Sj0Fp)!c_%efdv*=U{SyV3oNj}0*e9`SYUw#7FZOp ezyb>_r}6`v(9i0j^)C(p0000 deck1.modified + d2mod = deck2.modified + assert deck1.lastSync == 0 and deck2.lastSync == 0 + client.sync() + assert deck1.modified == deck2.modified + assert deck1.lastSync == deck1.modified + assert deck1.lastSync == deck2.lastSync + # ensure values are being synced + deck1.lowPriority += u",foo" + deck1.updateAllPriorities() + deck1.setModified() + client.sync() + assert "foo" in deck2.lowPriority + assert deck1.modified == deck2.modified + assert deck1.lastSync == deck2.lastSync + deck2.description = u"newname" + deck2.setModified() + client.sync() + assert deck1.description == u"newname" + # the most recent change should take precedence + deck1.description = u"foo" + deck1.setModified() + deck2.description = u"bar" + deck2.setModified() + client.sync() + assert deck1.description == "bar" + # answer a card to ensure stats & history are copied + c = deck1.getCard() + deck1.answerCard(c, 4) + client.sync() + assert dailyStats(deck2.s).reps == 1 + assert globalStats(deck2.s).reps == 1 + assert deck2.s.scalar("select count(id) from reviewHistory") == 1 + +@nose.with_setup(setup_local, teardown) +def test_localsync_models(): + client.sync() + # add a model + deck1.addModel(JapaneseModel()) + assert len(deck1.models) == 3 + assert len(deck2.models) == 2 + client.sync() + assert len(deck2.models) == 3 + assert deck1.currentModel.id == deck2.currentModel.id + # delete the recently added model + deck2.deleteModel(deck2.currentModel) + assert len(deck2.models) == 2 + client.sync() + assert len(deck1.models) == 2 + assert deck1.currentModel.id == deck2.currentModel.id + # make a card model inactive + assert deck1.currentModel.cardModels[1].active == True + deck2.currentModel.cardModels[1].active = False + deck2.currentModel.setModified() + deck2.setModified() + client.sync() + assert deck1.currentModel.cardModels[1].active == False + # remove a card model + deck1.deleteCardModel(deck1.currentModel, + deck1.currentModel.cardModels[1]) + deck1.currentModel.setModified() + deck1.setModified() + assert len(deck1.currentModel.cardModels) == 1 + client.sync() + assert len(deck2.currentModel.cardModels) == 1 + # add a field + c = deck1.getCard() + deck1.addFieldModel(c.fact.model, FieldModel(u"yo")) + deck1.s.refresh(c.fact) + assert len(c.fact.fields) == 3 + assert c.fact['yo'] == u"" + client.sync() + c2 = deck2.s.query(Card).get(c.id) + deck2.s.refresh(c2.fact) + assert c2.fact['yo'] == u"" + # remove the field + assert "yo" in c2.fact.keys() + deck2.deleteFieldModel(c2.fact.model, c2.fact.model.fieldModels[2]) + deck2.s.refresh(c2.fact) + assert "yo" not in c2.fact.keys() + client.sync() + deck1.s.refresh(c.fact) + assert "yo" not in c.fact.keys() + # rename a field + assert u"Front" in c.fact.keys() + deck1.renameFieldModel(deck1.currentModel, + deck1.currentModel.fieldModels[0], + u"Sideways") + client.sync() + assert deck2.currentModel.fieldModels[0].name == u"Sideways" + +@nose.with_setup(setup_local, teardown) +def test_localsync_factsandcards(): + assert deck1.factCount() == 1 and deck1.cardCount() == 2 + assert deck2.factCount() == 1 and deck2.cardCount() == 2 + client.sync() + assert deck1.factCount() == 2 and deck1.cardCount() == 4 + assert deck2.factCount() == 2 and deck2.cardCount() == 4 + # ensure the fact was copied across + f1 = deck1.s.query(Fact).first() + f2 = deck1.s.query(Fact).get(f1.id) + f1['Front'] = u"myfront" + f1.setModified() + deck1.setModified() + client.sync() + f2 = deck1.s.query(Fact).get(f1.id) + assert f2['Front'] == u"myfront" + deck1.rebuildQueue() + deck2.rebuildQueue() + c1 = deck1.getCard() + c2 = deck2.getCard() + assert c1.id == c2.id + +@nose.with_setup(setup_local, teardown) +def test_localsync_threeway(): + # deck1 (client) <-> deck2 (server) <-> deck3 (client) + deck3 = DeckStorage.Deck() + client2 = SyncClient(deck3) + server2 = SyncServer(deck2) + client2.setServer(server2) + client.sync() + client2.sync() + # add a new question + f = deck1.newFact() + f['Front'] = u"a"; f['Back'] = u"b" + cards = deck1.addFact(f) + card = cards[0] + client.sync() + assert deck1.cardCount() == 6 + assert deck2.cardCount() == 6 + # check it propagates from server to deck3 + client2.sync() + assert deck3.cardCount() == 6 + # delete a card on deck1 + deck1.deleteCard(card.id) + client.sync() + assert deck1.cardCount() == 5 + assert deck2.cardCount() == 5 + # make sure the delete is now propagated from the server to deck3 + client2.sync() + assert deck3.cardCount() == 5 + +def test_localsync_media(): + tmpdir = "/tmp/media-tests" + try: + shutil.rmtree(tmpdir) + except OSError: + pass + shutil.copytree(os.path.join(os.getcwd(), + "tests/syncing/media-tests"), + tmpdir) + deck1anki = os.path.join(tmpdir, "1.anki") + deck2anki = os.path.join(tmpdir, "2.anki") + deck1media = os.path.join(tmpdir, "1.media") + deck2media = os.path.join(tmpdir, "2.media") + setup_local((deck1anki, deck2anki)) + assert len(os.listdir(deck1media)) == 2 + assert len(os.listdir(deck2media)) == 1 + client.sync() + assert len(os.listdir(deck1media)) == 3 + assert len(os.listdir(deck2media)) == 3 + assert deck1.s.scalar("select count(1) from media") == 3 + assert deck2.s.scalar("select count(1) from media") == 3 + # check delete + os.unlink(os.path.join(deck1media, "22161b29b0c18e068038021f54eee1ee.png")) + rebuildMediaDir(deck1) + client.sync() + assert deck1.s.scalar("select count(1) from media") == 2 + assert deck2.s.scalar("select count(1) from media") == 2 + +# Remote tests +########################################################################## + +# a replacement runCmd which just calls our server directly +def runCmd(action, *args, **kargs): + #print action, kargs + return server.unstuff(apply(getattr(server, action), tuple(args) + + tuple(kargs.values()))) + +def setup_remote(): + setup_local() + global client, server + proxy = HttpSyncServerProxy("test", "foo") + client = SyncClient(deck1) + client.setServer(proxy) + proxy.deckName = "test" + proxy.runCmd = runCmd + server = HttpSyncServer() + server.deck = deck2 + server.decks = {"test": (deck2.modified, 0)} + +@nose.with_setup(setup_remote, teardown) +def test_remotesync_fromserver(): + # deck two was modified last + assert deck2.modified > deck1.modified + client.sync() + assert deck2.modified == deck1.modified + +@nose.with_setup(setup_remote, teardown) +def test_remotesync_toserver(): + deck1.setModified() + client.sync() + assert deck2.modified == deck1.modified diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..7427ac00d --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,18 @@ +# coding: utf-8 + +import nose, os +from tests.shared import assertException + +from anki.errors import * +from anki.facts import * +from anki import DeckStorage +from anki.utils import * + + +def test_tags(): + card = "one, two" + fact = "two,three, two" + cmodel = "four" + + assert (sorted(parseTags(mergeTags(card, fact, cmodel))) == + ['four', 'one', 'three', 'two']) diff --git a/tools/tests.sh b/tools/tests.sh new file mode 100755 index 000000000..51f3d3dda --- /dev/null +++ b/tools/tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +if [ -d 'locale' ]; then + dir=.. +else + dir=. +fi +(cd $dir && nosetests -vs $@) diff --git a/tools/translate.sh b/tools/translate.sh new file mode 100755 index 000000000..4c656df1f --- /dev/null +++ b/tools/translate.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# update translation files +# + +if [ ! -d "locale" ] +then + echo "Please run this from the anki module directory" + exit +fi + +allPyFiles=libanki.files +echo "Generating translations.." +for i in *.py features/*.py +do + echo $i >> $allPyFiles +done + +xgettext -s --no-wrap --files-from=$allPyFiles --output=locale/messages.pot +for file in locale/*.po +do + echo -n $file + msgmerge -s --no-wrap $file locale/messages.pot > $file.new && mv $file.new $file + outdir=$(echo $file | \ + perl -pe 's%locale/libanki_(.*)\.po%locale/\1/LC_MESSAGES%') + outfile="$outdir/libanki.mo" + mkdir -p $outdir + msgfmt $file --output-file=$outfile +done +rm $allPyFiles